diff --git a/lerna.json b/lerna.json index 75139c252301e9..09625182134e58 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.4.2-canary.24" + "version": "15.4.2-canary.25" } diff --git a/package.json b/package.json index b3d8dc3ae6a535..ec144e0cc476a6 100644 --- a/package.json +++ b/package.json @@ -233,16 +233,16 @@ "pretty-ms": "7.0.0", "random-seed": "0.3.0", "react": "19.0.0", - "react-builtin": "npm:react@19.2.0-canary-9784cb37-20250730", + "react-builtin": "npm:react@19.2.0-canary-c260b38d-20250731", "react-dom": "19.0.0", - "react-dom-builtin": "npm:react-dom@19.2.0-canary-9784cb37-20250730", - "react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-9784cb37-20250730", - "react-experimental-builtin": "npm:react@0.0.0-experimental-9784cb37-20250730", - "react-is-builtin": "npm:react-is@19.2.0-canary-9784cb37-20250730", - "react-server-dom-turbopack": "19.2.0-canary-9784cb37-20250730", - "react-server-dom-turbopack-experimental": "npm:react-server-dom-turbopack@0.0.0-experimental-9784cb37-20250730", - "react-server-dom-webpack": "19.2.0-canary-9784cb37-20250730", - "react-server-dom-webpack-experimental": "npm:react-server-dom-webpack@0.0.0-experimental-9784cb37-20250730", + "react-dom-builtin": "npm:react-dom@19.2.0-canary-c260b38d-20250731", + "react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-c260b38d-20250731", + "react-experimental-builtin": "npm:react@0.0.0-experimental-c260b38d-20250731", + "react-is-builtin": "npm:react-is@19.2.0-canary-c260b38d-20250731", + "react-server-dom-turbopack": "19.2.0-canary-c260b38d-20250731", + "react-server-dom-turbopack-experimental": "npm:react-server-dom-turbopack@0.0.0-experimental-c260b38d-20250731", + "react-server-dom-webpack": "19.2.0-canary-c260b38d-20250731", + "react-server-dom-webpack-experimental": "npm:react-server-dom-webpack@0.0.0-experimental-c260b38d-20250731", "react-ssr-prepass": "1.0.8", "react-virtualized": "9.22.3", "relay-compiler": "13.0.2", @@ -252,8 +252,8 @@ "resolve-from": "5.0.0", "sass": "1.54.0", "satori": "0.15.2", - "scheduler-builtin": "npm:scheduler@0.27.0-canary-9784cb37-20250730", - "scheduler-experimental-builtin": "npm:scheduler@0.0.0-experimental-9784cb37-20250730", + "scheduler-builtin": "npm:scheduler@0.27.0-canary-c260b38d-20250731", + "scheduler-experimental-builtin": "npm:scheduler@0.0.0-experimental-c260b38d-20250731", "seedrandom": "3.0.5", "semver": "7.3.7", "serve-handler": "6.1.6", @@ -297,10 +297,10 @@ "@types/react-dom": "19.1.6", "@types/retry": "0.12.0", "jest-snapshot": "30.0.0-alpha.6", - "react": "19.2.0-canary-9784cb37-20250730", - "react-dom": "19.2.0-canary-9784cb37-20250730", - "react-is": "19.2.0-canary-9784cb37-20250730", - "scheduler": "0.27.0-canary-9784cb37-20250730" + "react": "19.2.0-canary-c260b38d-20250731", + "react-dom": "19.2.0-canary-c260b38d-20250731", + "react-is": "19.2.0-canary-c260b38d-20250731", + "scheduler": "0.27.0-canary-c260b38d-20250731" }, "patchedDependencies": { "webpack-sources@3.2.3": "patches/webpack-sources@3.2.3.patch", diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index afa1eb3f82e123..3e257e7dc1e7f1 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.4.2-canary.24", + "version": "15.4.2-canary.25", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 1207b4298b8769..3d0b6657461f0b 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.4.2-canary.24", + "version": "15.4.2-canary.25", "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.4.2-canary.24", + "@next/eslint-plugin-next": "15.4.2-canary.25", "@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-internal/package.json b/packages/eslint-plugin-internal/package.json index 38b2c64d4ed9de..630a67f1c754d6 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 2c4c7f943d058d..11c628f4dd771a 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.4.2-canary.24", + "version": "15.4.2-canary.25", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index 983c848a47db68..48fd3541c72e22 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "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 16d6438485b6ea..542dc58d8caca8 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.4.2-canary.24", + "version": "15.4.2-canary.25", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 1447ab968d28b1..11cf74082602b3 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 212e8d9b6c9dd1..deee627b0e5e01 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index f45ed8bbeff12c..0f08e85e41589e 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 58c1229724cfbf..fb60c4f1c1d6fd 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.4.2-canary.24", + "version": "15.4.2-canary.25", "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 b94c8e9c4e0734..f4b789553754e1 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.4.2-canary.24", + "version": "15.4.2-canary.25", "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 e6fadc4458d522..6c364b516cb3f2 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.4.2-canary.24", + "version": "15.4.2-canary.25", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 759bdda3425c79..b17349bcd8547b 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 75b2d0ecac4719..98046fbbf620a6 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "private": true, "files": [ "native/" diff --git a/packages/next/errors.json b/packages/next/errors.json index ed3dbad606597f..b3c2e089080c1a 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -764,5 +764,11 @@ "763": "\\`unstable_rootParams\\` must not be used within a client component. Next.js should be preventing it from being included in client components statically, but did not in this case.", "764": "Missing workStore in %s", "765": "Route %s used %s inside a Route Handler. Support for this API in Route Handlers is planned for a future version of Next.js.", - "766": "%s was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route." + "766": "%s was used inside a Server Action. This is not supported. Functions from 'next/root-params' can only be called in the context of a route.", + "767": "A runtime prerender store should not be used for a route handler.", + "768": "createPrerenderSearchParamsForClientPage should not be called in a runtime prerender.", + "769": "createSearchParamsFromClient should not be called in a runtime prerender.", + "770": "createParamsFromClient should not be called in a runtime prerender.", + "771": "\\`%s\\` was called during a runtime prerender. Next.js should be preventing %s from being included in server components statically, but did not in this case.", + "772": "FetchStrategy.PPRRuntime should never be used when `experimental.clientSegmentCache` is disabled" } diff --git a/packages/next/package.json b/packages/next/package.json index dfc9ae3f46a47c..4a3d439fb4a9e3 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -102,7 +102,7 @@ ] }, "dependencies": { - "@next/env": "15.4.2-canary.24", + "@next/env": "15.4.2-canary.25", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -166,11 +166,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.4.2-canary.24", - "@next/polyfill-module": "15.4.2-canary.24", - "@next/polyfill-nomodule": "15.4.2-canary.24", - "@next/react-refresh-utils": "15.4.2-canary.24", - "@next/swc": "15.4.2-canary.24", + "@next/font": "15.4.2-canary.25", + "@next/polyfill-module": "15.4.2-canary.25", + "@next/polyfill-nomodule": "15.4.2-canary.25", + "@next/react-refresh-utils": "15.4.2-canary.25", + "@next/swc": "15.4.2-canary.25", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.51.1", "@rspack/core": "1.4.5", diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index bc14749d10ab36..7ee24b6672ea33 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -192,7 +192,7 @@ export async function handler( */ const isPrefetchRSCRequest = getRequestMeta(req, 'isPrefetchRSCRequest') ?? - Boolean(req.headers[NEXT_ROUTER_PREFETCH_HEADER]) + req.headers[NEXT_ROUTER_PREFETCH_HEADER] === '1' // exclude runtime prefetches, which use '2' // NOTE: Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later diff --git a/packages/next/src/client/app-dir/form.tsx b/packages/next/src/client/app-dir/form.tsx index 3639f5d469e438..ad4aecf2fefe06 100644 --- a/packages/next/src/client/app-dir/form.tsx +++ b/packages/next/src/client/app-dir/form.tsx @@ -61,6 +61,7 @@ export default function Form({ } } + // TODO(runtime-ppr): allow runtime prefetches in Form const prefetch = prefetchProp === false || prefetchProp === null ? prefetchProp : null diff --git a/packages/next/src/client/app-dir/link.tsx b/packages/next/src/client/app-dir/link.tsx index 9c3b1e53dd8132..6b67faf4186f15 100644 --- a/packages/next/src/client/app-dir/link.tsx +++ b/packages/next/src/client/app-dir/link.tsx @@ -20,7 +20,10 @@ import { import { isLocalURL } from '../../shared/lib/router/utils/is-local-url' import { dispatchNavigateAction } from '../components/app-router-instance' import { errorOnce } from '../../shared/lib/utils/error-once' -import { FetchStrategy } from '../components/segment-cache' +import { + FetchStrategy, + type PrefetchTaskFetchStrategy, +} from '../components/segment-cache' type Url = string | UrlObject type RequiredKeys = { @@ -151,10 +154,10 @@ type InternalLinkProps = { * * ``` */ - prefetch?: boolean | 'auto' | null + prefetch?: boolean | 'auto' | null | 'unstable_forceStale' /** - * (unstable) Switch to a dynamic prefetch on hover. Effectively the same as + * (unstable) Switch to a full prefetch on hover. Effectively the same as * updating the prefetch prop to `true` in a mouse event. */ unstable_dynamicOnHover?: boolean @@ -356,18 +359,12 @@ export default function LinkComponent( const router = React.useContext(AppRouterContext) const prefetchEnabled = prefetchProp !== false - /** - * The possible states for prefetch are: - * - null: this is the default "auto" mode, where we will prefetch partially if the link is in the viewport - * - true: we will prefetch if the link is visible and prefetch the full page, not just partially - * - false: we will not prefetch if in the viewport at all - * - 'unstable_dynamicOnHover': this starts in "auto" mode, but switches to "full" when the link is hovered - */ + const fetchStrategy = - prefetchProp === null || prefetchProp === 'auto' - ? // We default to PPR. We'll discover whether or not the route supports it with the initial prefetch. + prefetchProp !== false + ? getFetchStrategyFromPrefetchProp(prefetchProp) + : // TODO: it makes no sense to assign a fetchStrategy when prefetching is disabled. FetchStrategy.PPR - : FetchStrategy.Full if (process.env.NODE_ENV !== 'production') { function createPropError(args: { @@ -470,11 +467,12 @@ export default function LinkComponent( if ( props[key] != null && valType !== 'boolean' && - props[key] !== 'auto' + props[key] !== 'auto' && + props[key] !== 'unstable_forceStale' ) { throw createPropError({ key, - expected: '`boolean | "auto"`', + expected: '`boolean | "auto" | "unstable_forceStale"`', actual: valType, }) } @@ -745,3 +743,41 @@ const LinkStatusContext = createContext< export const useLinkStatus = () => { return useContext(LinkStatusContext) } + +function getFetchStrategyFromPrefetchProp( + prefetchProp: Exclude +): PrefetchTaskFetchStrategy { + if ( + process.env.__NEXT_CACHE_COMPONENTS && + process.env.__NEXT_CLIENT_SEGMENT_CACHE + ) { + // In the new implementation: + // - `prefetch={true}` is a runtime prefetch + // (includes cached IO + params + cookies, with dynamic holes for uncached IO). + // - `unstable_forceStale` is a "full" prefetch + // (forces inclusion of all dynamic data, i.e. the old behavior of `prefetch={true}`) + if (prefetchProp === true) { + return FetchStrategy.PPRRuntime + } + if (prefetchProp === 'unstable_forceStale') { + return FetchStrategy.Full + } + + // `null` or `"auto"`: this is the default "auto" mode, where we will prefetch partially if the link is in the viewport. + // This will also include invalid prop values that don't match the types specified here. + // (although those should've been filtered out by prop validation in dev) + prefetchProp satisfies null | 'auto' + // In `clientSegmentCache`, we default to PPR, and we'll discover whether or not the route supports it with the initial prefetch. + // If we're not using `clientSegmentCache`, this will be converted into a `PrefetchKind.AUTO`. + return FetchStrategy.PPR + } else { + return prefetchProp === null || prefetchProp === 'auto' + ? // In `clientSegmentCache`, we default to PPR, and we'll discover whether or not the route supports it with the initial prefetch. + // If we're not using `clientSegmentCache`, this will be converted into a `PrefetchKind.AUTO`. + FetchStrategy.PPR + : // In the old implementation without runtime prefetches, `prefetch={true}` forces all dynamic data to be prefetched. + // To preserve backwards-compatibility, anything other than `false`, `null`, or `"auto"` results in a full prefetch. + // (although invalid values should've been filtered out by prop validation in dev) + FetchStrategy.Full + } +} diff --git a/packages/next/src/client/components/app-router-instance.ts b/packages/next/src/client/components/app-router-instance.ts index a431a9e3a3505d..956257aab3bc95 100644 --- a/packages/next/src/client/components/app-router-instance.ts +++ b/packages/next/src/client/components/app-router-instance.ts @@ -329,6 +329,8 @@ export const publicAppRouterInstance: AppRouterInstance = { const actionQueue = getAppRouterActionQueue() const prefetchKind = options?.kind ?? PrefetchKind.AUTO + // We don't currently offer a way to issue a runtime prefetch via `router.prefetch()`. + // This will be possible when we update its API to not take a PrefetchKind. let fetchStrategy: PrefetchTaskFetchStrategy switch (prefetchKind) { case PrefetchKind.AUTO: { diff --git a/packages/next/src/client/components/bailout-to-client-rendering.ts b/packages/next/src/client/components/bailout-to-client-rendering.ts index eadd2e66a275e0..7041bfe67a24a0 100644 --- a/packages/next/src/client/components/bailout-to-client-rendering.ts +++ b/packages/next/src/client/components/bailout-to-client-rendering.ts @@ -12,6 +12,7 @@ export function bailoutToClientRendering(reason: string): void | never { if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': case 'prerender-client': case 'prerender-ppr': case 'prerender-legacy': diff --git a/packages/next/src/client/components/links.ts b/packages/next/src/client/components/links.ts index 378078407ee139..1bc61f8b50f1f0 100644 --- a/packages/next/src/client/components/links.ts +++ b/packages/next/src/client/components/links.ts @@ -17,6 +17,7 @@ import { } from './segment-cache' import { startTransition } from 'react' import { PrefetchKind } from './router-reducer/router-reducer-types' +import { InvariantError } from '../../shared/lib/invariant-error' type LinkElement = HTMLAnchorElement | SVGAElement @@ -264,7 +265,7 @@ export function onNavigationIntent( process.env.__NEXT_DYNAMIC_ON_HOVER && unstable_upgradeToDynamicPrefetch ) { - // Switch to a full, dynamic prefetch + // Switch to a full prefetch instance.fetchStrategy = FetchStrategy.Full } rescheduleLinkPrefetch(instance, PrefetchPriority.Intent) @@ -378,6 +379,13 @@ function prefetchWithOldCacheImplementation(instance: PrefetchableInstance) { prefetchKind = PrefetchKind.FULL break } + case FetchStrategy.PPRRuntime: { + // We can only get here if Client Segment Cache is off, and in that case + // it shouldn't be possible for a link to request a runtime prefetch. + throw new InvariantError( + 'FetchStrategy.PPRRuntime should never be used when `experimental.clientSegmentCache` is disabled' + ) + } default: { instance.fetchStrategy satisfies never // Unreachable, but otherwise typescript will consider the variable unassigned diff --git a/packages/next/src/client/components/navigation-untracked.ts b/packages/next/src/client/components/navigation-untracked.ts index 335f6e92e8e94c..e6987c30fb7b96 100644 --- a/packages/next/src/client/components/navigation-untracked.ts +++ b/packages/next/src/client/components/navigation-untracked.ts @@ -24,6 +24,7 @@ function hasFallbackRouteParams(): boolean { return fallbackParams ? fallbackParams.size > 0 : false case 'prerender-legacy': case 'request': + case 'prerender-runtime': case 'cache': case 'private-cache': case 'unstable-cache': diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts index 341538860cb14a..adfa6e636994f3 100644 --- a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts +++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts @@ -37,7 +37,26 @@ export function createInitialRouterState({ // This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it // as a URL that should be crawled. const initialCanonicalUrl = initialCanonicalUrlParts.join('/') - const normalizedFlightData = getFlightDataPartsFromPath(initialFlightData[0]) + + // TODO: Eventually we want to always read the rendered params from the URL + // and/or the x-rewritten-path header, so that we can omit them from the + // response body. This lets reuse cached responses if params aren't referenced + // anywhere in the actual page data, e.g. if they're only accessed by client + // components. However, during the initial render, there's no way to access + // the headers. For a partially dynamic page, this is OK, because there's + // going to be a dynamic server render regardless, so we can send the URL + // in the resume body. For a completely static page, though, there's no + // dynamic server render, so that won't work. + // + // Instead, we'll perform a HEAD request and read the rewritten URL from + // that response. + const renderedPathname = new URL(initialCanonicalUrl, 'http://localhost') + .pathname + + const normalizedFlightData = getFlightDataPartsFromPath( + initialFlightData[0], + renderedPathname + ) const { tree: initialTree, seedData: initialSeedData, diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index ee90122beee0b1..eaa298696a9b26 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -31,6 +31,7 @@ import { } from '../../flight-data-helpers' import { getAppBuildId } from '../../app-build-id' import { setCacheBustingSearchParam } from './set-cache-busting-search-param' +import { getRenderedPathname } from '../../route-params' const createFromReadableStream = createFromReadableStreamBrowser as (typeof import('react-server-dom-webpack/client.browser'))['createFromReadableStream'] @@ -55,7 +56,7 @@ export type RequestHeaders = { [RSC_HEADER]?: '1' [NEXT_ROUTER_STATE_TREE_HEADER]?: string [NEXT_URL]?: string - [NEXT_ROUTER_PREFETCH_HEADER]?: '1' + [NEXT_ROUTER_PREFETCH_HEADER]?: '1' | '2' [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]?: string 'x-deployment-id'?: string [NEXT_HMR_REFRESH_HEADER]?: '1' @@ -235,8 +236,21 @@ export async function fetchServerResponse( return doMpaNavigation(res.url) } + let renderedPathname + if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) { + // Read the URL from the response object. + renderedPathname = getRenderedPathname(res) + } else { + // Before Segment Cache is enabled, we should not rely on the new + // rewrite headers (x-rewritten-path, x-rewritten-query) because that + // is a breaking change. Read the URL from the response body. + const renderedUrlParts = response.c + renderedPathname = new URL(renderedUrlParts.join('/'), 'http://localhost') + .pathname + } + return { - flightData: normalizeFlightData(response.f), + flightData: normalizeFlightData(response.f, renderedPathname), canonicalUrl: canonicalUrl, couldBeIntercepted: interception, prerendered: response.S, diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index 72981c3ec9a3af..3ad2815b5a4ef0 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -55,6 +55,7 @@ import { omitUnusedArgs, } from '../../../../shared/lib/server-reference-info' import { revalidateEntireCache } from '../../segment-cache' +import { getRenderedPathname } from '../../../route-params' const createFromFetch = createFromFetchBrowser as (typeof import('react-server-dom-webpack/client.browser'))['createFromFetch'] @@ -180,9 +181,25 @@ async function fetchServerAction( Promise.resolve(res), { callServer, findSourceMapURL, temporaryReferences } ) + + let renderedPathname + if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) { + // Read the URL from the response object. + renderedPathname = getRenderedPathname(res) + } else { + // Before Segment Cache is enabled, we should not rely on the new + // rewrite headers (x-rewritten-path, x-rewritten-query) because that + // is a breaking change. Read the URL from the response body. + const canonicalUrlParts = response.c + renderedPathname = new URL( + canonicalUrlParts.join('/'), + 'http://localhost' + ).pathname + } + // An internal redirect can send an RSC response, but does not have a useful `actionResult`. actionResult = redirectLocation ? undefined : response.a - actionFlightData = normalizeFlightData(response.f) + actionFlightData = normalizeFlightData(response.f, renderedPathname) } else { // An external redirect doesn't contain RSC data. actionResult = undefined diff --git a/packages/next/src/client/components/segment-cache-impl/cache-key.ts b/packages/next/src/client/components/segment-cache-impl/cache-key.ts index 83ebd98df57cde..5a15113cebeb85 100644 --- a/packages/next/src/client/components/segment-cache-impl/cache-key.ts +++ b/packages/next/src/client/components/segment-cache-impl/cache-key.ts @@ -1,6 +1,3 @@ -import { NEXT_REWRITTEN_QUERY_HEADER } from '../app-router-headers' -import type { RSCResponse } from '../router-reducer/fetch-server-response' - // TypeScript trick to simulate opaque types, like in Flow. type Opaque = T & { __brand: K } @@ -32,18 +29,3 @@ export function createCacheKey( } as RouteCacheKey return cacheKey } - -export function getRenderedSearch(response: RSCResponse): NormalizedSearch { - // If the server performed a rewrite, the search params used to render the - // page will be different from the params in the request URL. In this case, - // the response will include a header that gives the rewritten search query. - const rewrittenQuery = response.headers.get(NEXT_REWRITTEN_QUERY_HEADER) - if (rewrittenQuery !== null) { - return ( - rewrittenQuery === '' ? '' : '?' + rewrittenQuery - ) as NormalizedSearch - } - // If the header is not present, there was no rewrite, so we use the search - // query of the response URL. - return new URL(response.url).search as NormalizedSearch -} diff --git a/packages/next/src/client/components/segment-cache-impl/cache.ts b/packages/next/src/client/components/segment-cache-impl/cache.ts index 9d01b573cc0fd4..a69461c4c833fd 100644 --- a/packages/next/src/client/components/segment-cache-impl/cache.ts +++ b/packages/next/src/client/components/segment-cache-impl/cache.ts @@ -9,6 +9,7 @@ import type { } from '../../../shared/lib/app-router-context.shared-runtime' import type { CacheNodeSeedData, + DynamicParamTypesShort, Segment as FlightRouterStateSegment, } from '../../../server/app-render/types' import { HasLoadingBoundary } from '../../../server/app-render/types' @@ -42,7 +43,13 @@ import type { NormalizedSearch, RouteCacheKey, } from './cache-key' -import { getRenderedSearch } from './cache-key' +import { + doesStaticSegmentAppearInURL, + getRenderedPathname, + getRenderedSearch, + parseDynamicParamFromURLPart, + type RouteParam, +} from '../../route-params' import { createTupleMap, type TupleMap, type Prefix } from './tuple-map' import { createLRU } from './lru' import { @@ -88,7 +95,10 @@ import { FetchStrategy } from '../segment-cache' export type RouteTree = { key: string + // TODO: Remove the `segment` field, now that it can be reconstructed + // from `param`. segment: FlightRouterStateSegment + param: RouteParam | null slots: null | { [parallelRouteKey: string]: RouteTree } @@ -414,7 +424,9 @@ export function getSegmentKeypathForTask( // the cache key, because the search params are treated as dynamic data. The // cache entry is valid for all possible search param values. const isDynamicTask = - task.fetchStrategy === FetchStrategy.Full || !route.isPPREnabled + task.fetchStrategy === FetchStrategy.Full || + task.fetchStrategy === FetchStrategy.PPRRuntime || + !route.isPPREnabled return isDynamicTask && path.endsWith('/' + PAGE_SEGMENT_KEY) ? [path, route.renderedSearch] : [path] @@ -639,11 +651,21 @@ export function upsertSegmentEntry( // this function and confirming it's the same as `existingEntry`. const existingEntry = readExactSegmentCacheEntry(now, keypath) if (existingEntry !== null) { - if (candidateEntry.isPartial && !existingEntry.isPartial) { - // Don't replace a full segment with a partial one. A case where this - // might happen is if the existing segment was fetched via - // . - + // Don't replace a more specific segment with a less-specific one. A case where this + // might happen is if the existing segment was fetched via + // ``. + if ( + // We fetched the new segment using a different, less specific fetch strategy + // than the segment we already have in the cache, so it can't have more content. + (candidateEntry.fetchStrategy !== existingEntry.fetchStrategy && + !canNewFetchStrategyProvideMoreContent( + existingEntry.fetchStrategy, + candidateEntry.fetchStrategy + )) || + // The existing entry isn't partial, but the new one is. + // (TODO: can this be true if `candidateEntry.fetchStrategy >= existingEntry.fetchStrategy`?) + (!existingEntry.isPartial && candidateEntry.isPartial) + ) { // We're going to leave the entry on the owner's `revalidating` field // so that it doesn't get revalidated again unnecessarily. Downgrade the // Fulfilled entry to Rejected and null out the data so it can be garbage @@ -655,6 +677,7 @@ export function upsertSegmentEntry( rejectedEntry.rsc = null return null } + // Evict the existing entry from the cache. deleteSegmentFromCache(existingEntry, keypath) } @@ -856,19 +879,69 @@ function rejectSegmentCacheEntry( } } -function convertRootTreePrefetchToRouteTree(rootTree: RootTreePrefetch) { - return convertTreePrefetchToRouteTree(rootTree.tree, ROOT_SEGMENT_KEY) +function convertRootTreePrefetchToRouteTree( + rootTree: RootTreePrefetch, + renderedPathname: string +) { + // Remove trailing and leading slashes + const pathnameParts = renderedPathname.split('/').filter((p) => p !== '') + const index = 0 + return convertTreePrefetchToRouteTree( + rootTree.tree, + ROOT_SEGMENT_KEY, + pathnameParts, + index + ) } function convertTreePrefetchToRouteTree( prefetch: TreePrefetch, - key: string + key: string, + pathnameParts: Array, + pathnamePartsIndex: number ): RouteTree { // Converts the route tree sent by the server into the format used by the // cache. The cached version of the tree includes additional fields, such as a // cache key for each segment. Since this is frequently accessed, we compute // it once instead of on every access. This same cache key is also used to // request the segment from the server. + + let segment = prefetch.segment + + let doesAppearInURL: boolean + let param: RouteParam | null = null + if (Array.isArray(segment)) { + // This segment is parameterized. Get the param from the pathname. + const paramType = segment[2] as DynamicParamTypesShort + const paramValue = parseDynamicParamFromURLPart( + paramType, + pathnameParts, + pathnamePartsIndex + ) + param = { + name: segment[0], + value: paramValue, + type: paramType, + } + + // Assign a cache key to the segment, based on the param value. In the + // pre-Segment Cache implementation, the server computes this and sends it + // in the body of the response. In the Segment Cache implementation, the + // server sends an empty string and we fill it in here. + // TODO: This will land in a follow up PR. + // segment[1] = getCacheKeyForDynamicParam(paramValue) + + doesAppearInURL = true + } else { + doesAppearInURL = doesStaticSegmentAppearInURL(segment) + } + + // Only increment the index if the segment appears in the URL. If it's a + // "virtual" segment, like a route group, it remains the same. + const childPathnamePartsIndex = doesAppearInURL + ? pathnamePartsIndex + 1 + : pathnamePartsIndex + let slots: { [parallelRouteKey: string]: RouteTree } | null = null const prefetchSlots = prefetch.slots if (prefetchSlots !== null) { @@ -886,13 +959,17 @@ function convertTreePrefetchToRouteTree( ) slots[parallelRouteKey] = convertTreePrefetchToRouteTree( childPrefetch, - childKey + childKey, + pathnameParts, + childPathnamePartsIndex ) } } + return { key, - segment: prefetch.segment, + segment, + param, slots, isRootLayout: prefetch.isRootLayout, // This field is only relevant to dynamic routes. For a PPR/static route, @@ -940,26 +1017,39 @@ function convertFlightRouterStateToRouteTree( slots[parallelRouteKey] = childTree } } - - // The navigation implementation expects the search params to be included - // in the segment. However, in the case of a static response, the search - // params are omitted. So the client needs to add them back in when reading - // from the Segment Cache. - // - // For consistency, we'll do this for dynamic responses, too. - // - // TODO: We should move search params out of FlightRouterState and handle them - // entirely on the client, similar to our plan for dynamic params. const originalSegment = flightRouterState[0] - const segmentWithoutSearchParams = - typeof originalSegment === 'string' && - originalSegment.startsWith(PAGE_SEGMENT_KEY) - ? PAGE_SEGMENT_KEY - : originalSegment + + let segment: FlightRouterStateSegment + let param: RouteParam | null = null + if (Array.isArray(originalSegment)) { + const paramValue = originalSegment[3] + param = { + name: originalSegment[0], + value: paramValue === undefined ? null : paramValue, + type: originalSegment[2] as DynamicParamTypesShort, + } + segment = originalSegment + } else { + // The navigation implementation expects the search params to be included + // in the segment. However, in the case of a static response, the search + // params are omitted. So the client needs to add them back in when reading + // from the Segment Cache. + // + // For consistency, we'll do this for dynamic responses, too. + // + // TODO: We should move search params out of FlightRouterState and handle + // them entirely on the client, similar to our plan for dynamic params. + segment = + typeof originalSegment === 'string' && + originalSegment.startsWith(PAGE_SEGMENT_KEY) + ? PAGE_SEGMENT_KEY + : originalSegment + } return { key, - segment: segmentWithoutSearchParams, + segment, + param, slots, isRootLayout: flightRouterState[4] === true, hasLoadingBoundary: @@ -1144,15 +1234,21 @@ export async function fetchRouteOnCacheMiss( return null } - // Get the search params that were used to render the target page. This may - // be different from the search params in the request URL, if the page + // Get the params that were used to render the target page. These may + // be different from the params in the request URL, if the page // was rewritten. + const renderedPathname = getRenderedPathname(response) const renderedSearch = getRenderedSearch(response) + const routeTree = convertRootTreePrefetchToRouteTree( + serverData, + renderedPathname + ) + const staleTimeMs = serverData.staleTime * 1000 fulfillRouteCacheEntry( entry, - convertRootTreePrefetchToRouteTree(serverData), + routeTree, serverData.head, serverData.isHeadPartial, Date.now() + staleTimeMs, @@ -1355,7 +1451,10 @@ export async function fetchSegmentOnCacheMiss( export async function fetchSegmentPrefetchesUsingDynamicRequest( task: PrefetchTask, route: FulfilledRouteCacheEntry, - fetchStrategy: FetchStrategy.LoadingBoundary | FetchStrategy.Full, + fetchStrategy: + | FetchStrategy.LoadingBoundary + | FetchStrategy.PPRRuntime + | FetchStrategy.Full, dynamicRequestTree: FlightRouterState, spawnedEntries: Map ): Promise | null> { @@ -1370,13 +1469,26 @@ export async function fetchSegmentPrefetchesUsingDynamicRequest( if (nextUrl !== null) { headers[NEXT_URL] = nextUrl } - // Only set the prefetch header if we're not doing a "full" prefetch. We - // omit the prefetch header from a full prefetch because it's essentially - // just a navigation request that happens ahead of time — it should include - // all the same data in the response. - if (fetchStrategy !== FetchStrategy.Full) { - headers[NEXT_ROUTER_PREFETCH_HEADER] = '1' + switch (fetchStrategy) { + case FetchStrategy.Full: { + // We omit the prefetch header from a full prefetch because it's essentially + // just a navigation request that happens ahead of time — it should include + // all the same data in the response. + break + } + case FetchStrategy.PPRRuntime: { + headers[NEXT_ROUTER_PREFETCH_HEADER] = '2' + break + } + case FetchStrategy.LoadingBoundary: { + headers[NEXT_ROUTER_PREFETCH_HEADER] = '1' + break + } + default: { + fetchStrategy satisfies never + } } + try { const response = await fetchPrefetchResponse(url, headers) if (!response || !response.ok || !response.body) { @@ -1425,9 +1537,13 @@ export async function fetchSegmentPrefetchesUsingDynamicRequest( prefetchStream ) as Promise) - // Since we did not set the prefetch header, the response from the server - // will never contain dynamic holes. - const isResponsePartial = false + const isResponsePartial = + fetchStrategy === FetchStrategy.PPRRuntime + ? // A runtime prefetch may have holes. + !!response.headers.get(NEXT_DID_POSTPONE_HEADER) + : // Full and LoadingBoundary prefetches cannot have holes. + // (even if we did set the prefetch header, we only use this codepath for non-PPR-enabled routes) + false // Aside from writing the data into the cache, this function also returns // the entries that were fulfilled, so we can streamingly update their sizes @@ -1455,7 +1571,10 @@ export async function fetchSegmentPrefetchesUsingDynamicRequest( function writeDynamicTreeResponseIntoCache( now: number, task: PrefetchTask, - fetchStrategy: FetchStrategy.LoadingBoundary | FetchStrategy.Full, + fetchStrategy: + | FetchStrategy.LoadingBoundary + | FetchStrategy.PPRRuntime + | FetchStrategy.Full, response: RSCResponse, serverData: NavigationFlightResponse, entry: PendingRouteCacheEntry, @@ -1463,7 +1582,15 @@ function writeDynamicTreeResponseIntoCache( canonicalUrl: string, routeIsPPREnabled: boolean ) { - const normalizedFlightDataResult = normalizeFlightData(serverData.f) + // Get the URL that was used to render the target page. This may be different + // from the URL in the request URL, if the page was rewritten. + const renderedSearch = getRenderedSearch(response) + const renderedPathname = getRenderedPathname(response) + + const normalizedFlightDataResult = normalizeFlightData( + serverData.f, + renderedPathname + ) if ( // A string result means navigating to this route will result in an // MPA navigation. @@ -1497,11 +1624,6 @@ function writeDynamicTreeResponseIntoCache( const isResponsePartial = response.headers.get(NEXT_DID_POSTPONE_HEADER) === '1' - // Get the search params that were used to render the target page. This may - // be different from the search params in the request URL, if the page - // was rewritten. - const renderedSearch = getRenderedSearch(response) - const fulfilledEntry = fulfillRouteCacheEntry( entry, convertRootFlightRouterStateToRouteTree(flightRouterState), @@ -1553,7 +1675,10 @@ function rejectSegmentEntriesIfStillPending( function writeDynamicRenderResponseIntoCache( now: number, task: PrefetchTask, - fetchStrategy: FetchStrategy.LoadingBoundary | FetchStrategy.Full, + fetchStrategy: + | FetchStrategy.LoadingBoundary + | FetchStrategy.PPRRuntime + | FetchStrategy.Full, response: RSCResponse, serverData: NavigationFlightResponse, isResponsePartial: boolean, @@ -1571,7 +1696,12 @@ function writeDynamicRenderResponseIntoCache( } return null } - const flightDatas = normalizeFlightData(serverData.f) + + // Get the URL that was used to render the target page. This may be different + // from the URL in the request URL, if the page was rewritten. + const renderedPathname = getRenderedPathname(response) + + const flightDatas = normalizeFlightData(serverData.f, renderedPathname) if (typeof flightDatas === 'string') { // This means navigating to this route will result in an MPA navigation. // TODO: We should cache this, too, so that the MPA navigation is immediate. @@ -1661,7 +1791,10 @@ function writeDynamicRenderResponseIntoCache( function writeSeedDataIntoCache( now: number, task: PrefetchTask, - fetchStrategy: FetchStrategy.LoadingBoundary | FetchStrategy.Full, + fetchStrategy: + | FetchStrategy.LoadingBoundary + | FetchStrategy.PPRRuntime + | FetchStrategy.Full, route: FulfilledRouteCacheEntry, staleAt: number, seedData: CacheNodeSeedData, @@ -1855,3 +1988,31 @@ function createPromiseWithResolvers(): PromiseWithResolvers { }) return { resolve: resolve!, reject: reject!, promise } } + +/** + * Checks whether the new fetch strategy is likely to provide more content than the old one. + * + * Generally, when an app uses dynamic data, a "more specific" fetch strategy is expected to provide more content: + * - `LoadingBoundary` only provides static layouts + * - `PPR` can provide shells for each segment (even for segments that use dynamic data) + * - `PPRRuntime` can additionally include content that uses searchParams, params, or cookies + * - `Full` includes all the content, even if it uses dynamic data + * + * However, it's possible that a more specific fetch strategy *won't* give us more content if: + * - a segment is fully static + * (then, `PPR`/`PPRRuntime`/`Full` will all yield equivalent results) + * - providing searchParams/params/cookies doesn't reveal any more content, e.g. because of an `await connection()` + * (then, `PPR` and `PPRRuntime` will yield equivalent results, only `Full` will give us more) + * Because of this, when comparing two segments, we should also check if the existing segment is partial. + * If it's not partial, then there's no need to prefetch it again, even using a "more specific" strategy. + * There's currently no way to know if `PPRRuntime` will yield more data that `PPR`, so we have to assume it will. + * + * Also note that, in practice, we don't expect to be comparing `LoadingBoundary` to `PPR`/`PPRRuntime`, + * because a non-PPR-enabled route wouldn't ever use the latter strategies. It might however use `Full`. + */ +export function canNewFetchStrategyProvideMoreContent( + currentStrategy: FetchStrategy, + newStrategy: FetchStrategy +): boolean { + return currentStrategy < newStrategy +} diff --git a/packages/next/src/client/components/segment-cache-impl/scheduler.ts b/packages/next/src/client/components/segment-cache-impl/scheduler.ts index 3a8997058181ab..4f4dc7eb446dc2 100644 --- a/packages/next/src/client/components/segment-cache-impl/scheduler.ts +++ b/packages/next/src/client/components/segment-cache-impl/scheduler.ts @@ -25,13 +25,14 @@ import { waitForSegmentCacheEntry, resetRevalidatingSegmentEntry, getSegmentKeypathForTask, + canNewFetchStrategyProvideMoreContent, } from './cache' import type { RouteCacheKey } from './cache-key' import { - getCurrentCacheVersion, - PrefetchPriority, FetchStrategy, type PrefetchTaskFetchStrategy, + getCurrentCacheVersion, + PrefetchPriority, } from '../segment-cache' import { addSearchParamsIfPageSegment, @@ -193,6 +194,8 @@ let didScheduleMicrotask = false // priority at a time. We reserve special network bandwidth for this task only. let mostRecentlyHoveredLink: PrefetchTask | null = null +export type IncludeDynamicData = null | 'full' | 'dynamic' + /** * Initiates a prefetch task for the given URL. If a prefetch for the same URL * is already in progress, this will bump it to the top of the queue. @@ -510,8 +513,9 @@ function pingRootRouteTree( // a route. Currently we've only implemented the main one: per-segment, // static-data only. // - // There's also which prefetches both static *and* - // dynamic data. Similarly, we need to fallback to the old, per-page + // There's also `` + // which prefetch both static *and* dynamic data. + // Similarly, we need to fallback to the old, per-page // behavior if PPR is disabled for a route (via the incremental opt-in). // // Those cases will be handled here. @@ -560,6 +564,8 @@ function pingRootRouteTree( // A task's fetch strategy gets set to `PPR` for any "auto" prefetch. // If it turned out that the route isn't PPR-enabled, we need to use `LoadingBoundary` instead. + // We don't need to do this for runtime prefetches, because those are only available in + // `cacheComponents`, where every route is PPR. const fetchStrategy = task.fetchStrategy === FetchStrategy.PPR ? route.isPPREnabled @@ -574,6 +580,7 @@ function pingRootRouteTree( // enabled. It will not include any dynamic data. return pingPPRRouteTree(now, task, route, tree) case FetchStrategy.Full: + case FetchStrategy.PPRRuntime: case FetchStrategy.LoadingBoundary: { // Prefetch multiple segments using a single dynamic request. const spawnedEntries = new Map() @@ -679,7 +686,10 @@ function diffRouteTreeAgainstCurrent( oldTree: FlightRouterState, newTree: RouteTree, spawnedEntries: Map, - fetchStrategy: FetchStrategy.Full | FetchStrategy.LoadingBoundary + fetchStrategy: + | FetchStrategy.Full + | FetchStrategy.PPRRuntime + | FetchStrategy.LoadingBoundary ): FlightRouterState { // This is a single recursive traversal that does multiple things: // - Finds the parts of the target route (newTree) that are not part of @@ -755,6 +765,21 @@ function diffRouteTreeAgainstCurrent( requestTreeChildren[parallelRouteKey] = requestTreeChild break } + case FetchStrategy.PPRRuntime: { + // This is a runtime prefetch. Fetch all cacheable data in the tree, + // not just the static PPR shell. + const requestTreeChild = pingRouteTreeAndIncludeDynamicData( + now, + task, + route, + newTreeChild, + false, + spawnedEntries, + fetchStrategy + ) + requestTreeChildren[parallelRouteKey] = requestTreeChild + break + } case FetchStrategy.Full: { // This is a "full" prefetch. Fetch all the data in the tree, both // static and dynamic. We issue roughly the same request that we @@ -779,7 +804,8 @@ function diffRouteTreeAgainstCurrent( route, newTreeChild, false, - spawnedEntries + spawnedEntries, + fetchStrategy ) requestTreeChildren[parallelRouteKey] = requestTreeChild break @@ -867,7 +893,7 @@ function pingPPRDisabledRouteTreeUpToLoadingBoundary( // including it in this non-PPR request. // // We're intentionally choosing not to, though, because it's generally - // better to avoid doing a dynamic prefetch whenever possible. + // better to avoid doing a full prefetch whenever possible. break } case EntryStatus.Pending: { @@ -914,7 +940,8 @@ function pingRouteTreeAndIncludeDynamicData( route: FulfilledRouteCacheEntry, tree: RouteTree, isInsideRefetchingParent: boolean, - spawnedEntries: Map + spawnedEntries: Map, + fetchStrategy: FetchStrategy.Full | FetchStrategy.PPRRuntime ): FlightRouterState { // The tree we're constructing is the same shape as the tree we're navigating // to. But even though this is a "new" tree, some of the individual segments @@ -931,20 +958,30 @@ function pingRouteTreeAndIncludeDynamicData( switch (segment.status) { case EntryStatus.Empty: { // This segment is not cached. Include it in the request. - spawnedSegment = upgradeToPendingSegment(segment, FetchStrategy.Full) + spawnedSegment = upgradeToPendingSegment(segment, fetchStrategy) break } case EntryStatus.Fulfilled: { // The segment is already cached. - if (segment.isPartial) { - // The cached segment contians dynamic holes. Since this is a Full - // prefetch, we need to include it in the request. + if ( + segment.isPartial && + canNewFetchStrategyProvideMoreContent( + segment.fetchStrategy, + fetchStrategy + ) + ) { + // The cached segment contains dynamic holes, and was prefetched using a less specific strategy than the current one. + // This means we're in one of these cases: + // - we have a static prefetch, and we're doing a runtime prefetch + // - we have a static or runtime prefetch, and we're doing a Full prefetch (or a navigation). + // In either case, we need to include it in the request to get a more specific (or full) version. spawnedSegment = pingFullSegmentRevalidation( now, task, route, segment, - tree.key + tree.key, + fetchStrategy ) } break @@ -952,14 +989,20 @@ function pingRouteTreeAndIncludeDynamicData( case EntryStatus.Pending: case EntryStatus.Rejected: { // There's either another prefetch currently in progress, or the previous - // attempt failed. If it wasn't a Full prefetch, fetch it again. - if (segment.fetchStrategy !== FetchStrategy.Full) { + // attempt failed. If the new strategy can provide more content, fetch it again. + if ( + canNewFetchStrategyProvideMoreContent( + segment.fetchStrategy, + fetchStrategy + ) + ) { spawnedSegment = pingFullSegmentRevalidation( now, task, route, segment, - tree.key + tree.key, + fetchStrategy ) } break @@ -978,7 +1021,8 @@ function pingRouteTreeAndIncludeDynamicData( route, childTree, isInsideRefetchingParent || spawnedSegment !== null, - spawnedEntries + spawnedEntries, + fetchStrategy ) } } @@ -1027,6 +1071,7 @@ function pingPerSegment( // request it is, we may want to revalidate it. switch (segment.fetchStrategy) { case FetchStrategy.PPR: + case FetchStrategy.PPRRuntime: case FetchStrategy.Full: // There's already a request in progress. Don't do anything. break @@ -1059,6 +1104,7 @@ function pingPerSegment( // was originally fetched, we may or may not want to revalidate it. switch (segment.fetchStrategy) { case FetchStrategy.PPR: + case FetchStrategy.PPRRuntime: case FetchStrategy.Full: // The previous attempt to fetch this entry failed. Don't attempt to // fetch it again until the entry expires. @@ -1148,21 +1194,22 @@ function pingFullSegmentRevalidation( task: PrefetchTask, route: FulfilledRouteCacheEntry, currentSegment: SegmentCacheEntry, - segmentKey: string + segmentKey: string, + fetchStrategy: FetchStrategy.Full | FetchStrategy.PPRRuntime ): PendingSegmentCacheEntry | null { const revalidatingSegment = readOrCreateRevalidatingSegmentEntry( now, currentSegment ) if (revalidatingSegment.status === EntryStatus.Empty) { - // During a Full prefetch, a single dynamic request is made for all the + // During a Full/PPRRuntime prefetch, a single dynamic request is made for all the // segments that we need. So we don't initiate a request here directly. By // returning a pending entry from this function, it signals to the caller // that this segment should be included in the request that's sent to // the server. const pendingSegment = upgradeToPendingSegment( revalidatingSegment, - FetchStrategy.Full + fetchStrategy ) upsertSegmentOnCompletion( task, @@ -1174,15 +1221,20 @@ function pingFullSegmentRevalidation( } else { // There's already a revalidation in progress. const nonEmptyRevalidatingSegment = revalidatingSegment - if (nonEmptyRevalidatingSegment.fetchStrategy !== FetchStrategy.Full) { - // The existing revalidation was not fetched using the Full strategy. + if ( + canNewFetchStrategyProvideMoreContent( + nonEmptyRevalidatingSegment.fetchStrategy, + fetchStrategy + ) + ) { + // The existing revalidation was fetched using a less specific strategy. // Reset it and start a new revalidation. const emptySegment = resetRevalidatingSegmentEntry( nonEmptyRevalidatingSegment ) const pendingSegment = upgradeToPendingSegment( emptySegment, - FetchStrategy.Full + fetchStrategy ) upsertSegmentOnCompletion( task, diff --git a/packages/next/src/client/components/segment-cache.ts b/packages/next/src/client/components/segment-cache.ts index b6665fe4a06309..e2ad1fed065c23 100644 --- a/packages/next/src/client/components/segment-cache.ts +++ b/packages/next/src/client/components/segment-cache.ts @@ -19,6 +19,7 @@ export type { NavigationResult } from './segment-cache-impl/navigation' export type { PrefetchTask } from './segment-cache-impl/scheduler' +export type { NormalizedSearch } from './segment-cache-impl/cache-key' const notEnabled: any = () => { throw new Error( @@ -142,9 +143,13 @@ export const enum PrefetchPriority { } export const enum FetchStrategy { - PPR, - Full, - LoadingBoundary, + // Deliberately ordered so we can easily compare two segments + // and determine if one segment is "more specific" than another + // (i.e. if it's likely that it contains more data) + LoadingBoundary = 0, + PPR = 1, + PPRRuntime = 2, + Full = 3, } /** @@ -153,4 +158,7 @@ export const enum FetchStrategy { * until we complete the initial tree prefetch request, so we use `PPR` to signal both cases * and adjust it based on the route when actually fetching. * */ -export type PrefetchTaskFetchStrategy = FetchStrategy.PPR | FetchStrategy.Full +export type PrefetchTaskFetchStrategy = + | FetchStrategy.PPR + | FetchStrategy.PPRRuntime + | FetchStrategy.Full diff --git a/packages/next/src/client/flight-data-helpers.ts b/packages/next/src/client/flight-data-helpers.ts index 7ea616d23d5868..c241277036bfdf 100644 --- a/packages/next/src/client/flight-data-helpers.ts +++ b/packages/next/src/client/flight-data-helpers.ts @@ -1,5 +1,6 @@ import type { CacheNodeSeedData, + DynamicParamTypesShort, FlightData, FlightDataPath, FlightRouterState, @@ -8,6 +9,10 @@ import type { } from '../server/app-render/types' import type { HeadData } from '../shared/lib/app-router-context.shared-runtime' import { PAGE_SEGMENT_KEY } from '../shared/lib/segment' +import { + doesStaticSegmentAppearInURL, + parseDynamicParamFromURLPart, +} from './route-params' export type NormalizedFlightData = { /** @@ -31,7 +36,8 @@ export type NormalizedFlightData = { // we're currently exporting it so we can use it directly. This should be fixed as part of the unification of // the different ways we express `FlightSegmentPath`. export function getFlightDataPartsFromPath( - flightDataPath: FlightDataPath + flightDataPath: FlightDataPath, + renderedPathname: string ): NormalizedFlightData { // Pick the last 4 items from the `FlightDataPath` to get the [tree, seedData, viewport, isHeadPartial]. const flightDataPathLength = 4 @@ -41,6 +47,8 @@ export function getFlightDataPartsFromPath( // The `FlightSegmentPath` is everything except the last three items. For a root render, it won't be present. const segmentPath = flightDataPath.slice(0, -flightDataPathLength) + fillTreeWithParamValues(renderedPathname, segmentPath, tree) + return { // TODO: Unify these two segment path helpers. We are inconsistently pushing an empty segment ("") // to the start of the segment path in some places which makes it hard to use solely the segment path. @@ -67,7 +75,8 @@ export function getNextFlightSegmentPath( } export function normalizeFlightData( - flightData: FlightData + flightData: FlightData, + renderedPathname: string ): NormalizedFlightData[] | string { // FlightData can be a string when the server didn't respond with a proper flight response, // or when a redirect happens, to signal to the client that it needs to perform an MPA navigation. @@ -75,7 +84,9 @@ export function normalizeFlightData( return flightData } - return flightData.map(getFlightDataPartsFromPath) + return flightData.map((flightDataPath) => + getFlightDataPartsFromPath(flightDataPath, renderedPathname) + ) } /** @@ -169,3 +180,93 @@ function shouldPreserveRefreshMarker( ): boolean { return Boolean(refreshMarker && refreshMarker !== 'refresh') } + +function fillTreeWithParamValues( + renderedPathname: string, + segmentPath: FlightSegmentPath, + flightRouterState: FlightRouterState +): void { + // Traverse the FlightRouterState and fill in the param values using the + // rendered pathname. + + // Remove trailing and leading slashes, then split the pathname into parts. + // These will be assigned as params as we traverse the tree. + const pathnameParts = renderedPathname.split('/').filter((p) => p !== '') + + let pathnamePartsIndex = 0 + + // segmentPath represents the parent path of subtree. It's a repeating pattern + // of parallel route key and segment: + // + // [string, Segment, string, Segment, string, Segment, ...] + // + // Iterate through the path and skip over the corresponding pathname parts. + for (let i = 0; i < segmentPath.length; i += 2) { + const segment: Segment = segmentPath[i + 1] + if (Array.isArray(segment) || doesStaticSegmentAppearInURL(segment)) { + // This segment appears in the URL, so we need to skip over this part + // of the pathname + pathnamePartsIndex++ + } + } + + fillTreeWithParamValuesImpl( + renderedPathname, + flightRouterState, + pathnameParts, + pathnamePartsIndex + ) +} + +function fillTreeWithParamValuesImpl( + renderedPathname: string, + flightRouterState: FlightRouterState, + pathnameParts: Array, + pathnamePartsIndex: number +): void { + const segment = flightRouterState[0] + + let doesAppearInURL: boolean + if (Array.isArray(segment)) { + doesAppearInURL = true + + // This segment is parameterized. Get the param from the pathname. + const paramType = segment[2] as DynamicParamTypesShort + const paramValue = parseDynamicParamFromURLPart( + paramType, + pathnameParts, + pathnamePartsIndex + ) + + // Insert the param value into the segment. + // TODO: Eventually this is the value that will be passed to client + // components that render this param. + segment[3] = paramValue + + // Assign a cache key to the segment, based on the param value. In the + // pre-Segment Cache implementation, the server computes this and sends it + // in the body of the response. In the Segment Cache implementation, the + // server sends an empty string and we fill it in here. + // TODO: This will land in a follow up PR. + // const segmentCacheKey = getCacheKeyForDynamicParam(paramValue) + // segment[1] = segmentCacheKey + } else { + doesAppearInURL = doesStaticSegmentAppearInURL(segment) + } + + // Only increment the index if the segment appears in the URL. If it's a + // "virtual" segment, like a route group, it remains the same. + const childPathnamePartsIndex = doesAppearInURL + ? pathnamePartsIndex + 1 + : pathnamePartsIndex + + const parallelRoutes = flightRouterState[1] + for (const parallelRouteKey in parallelRoutes) { + fillTreeWithParamValuesImpl( + renderedPathname, + parallelRoutes[parallelRouteKey], + pathnameParts, + childPathnamePartsIndex + ) + } +} diff --git a/packages/next/src/client/link.tsx b/packages/next/src/client/link.tsx index ac05399ed07b6d..58d55153e80948 100644 --- a/packages/next/src/client/link.tsx +++ b/packages/next/src/client/link.tsx @@ -82,7 +82,7 @@ type InternalLinkProps = { * - `false`: Prefetching will not happen when entering the viewport, but will still happen on hover. * @defaultValue `true` (pages router) or `null` (app router) */ - prefetch?: boolean | 'auto' | null + prefetch?: boolean | 'auto' | null | 'unstable_forceStale' /** * The active locale is automatically prepended. `locale` allows for providing a different locale. * When `false` `href` has to include the locale as the default behavior is disabled. diff --git a/packages/next/src/client/route-params.ts b/packages/next/src/client/route-params.ts new file mode 100644 index 00000000000000..52e665ca68118f --- /dev/null +++ b/packages/next/src/client/route-params.ts @@ -0,0 +1,124 @@ +import type { DynamicParamTypesShort } from '../server/app-render/types' +import { PAGE_SEGMENT_KEY } from '../shared/lib/segment' +import { ROOT_SEGMENT_KEY } from '../shared/lib/segment-cache/segment-value-encoding' +import { + NEXT_REWRITTEN_PATH_HEADER, + NEXT_REWRITTEN_QUERY_HEADER, +} from './components/app-router-headers' +import type { RSCResponse } from './components/router-reducer/fetch-server-response' +import type { NormalizedSearch } from './components/segment-cache' + +type RouteParamValue = string | Array | null + +export type RouteParam = { + name: string + value: RouteParamValue + type: DynamicParamTypesShort +} + +export function getRenderedSearch(response: RSCResponse): NormalizedSearch { + // If the server performed a rewrite, the search params used to render the + // page will be different from the params in the request URL. In this case, + // the response will include a header that gives the rewritten search query. + const rewrittenQuery = response.headers.get(NEXT_REWRITTEN_QUERY_HEADER) + if (rewrittenQuery !== null) { + return ( + rewrittenQuery === '' ? '' : '?' + rewrittenQuery + ) as NormalizedSearch + } + // If the header is not present, there was no rewrite, so we use the search + // query of the response URL. + return new URL(response.url).search as NormalizedSearch +} + +export function getRenderedPathname(response: RSCResponse): string { + // If the server performed a rewrite, the pathname used to render the + // page will be different from the pathname in the request URL. In this case, + // the response will include a header that gives the rewritten pathname. + const rewrittenPath = response.headers.get(NEXT_REWRITTEN_PATH_HEADER) + return rewrittenPath ?? new URL(response.url).pathname +} + +export function parseDynamicParamFromURLPart( + paramType: DynamicParamTypesShort, + pathnameParts: Array, + partIndex: number +): RouteParamValue { + // This needs to match the behavior in get-dynamic-param.ts. + switch (paramType) { + // Catchalls + case 'c': + case 'ci': { + // Catchalls receive all the remaining URL parts. If there are no + // remaining pathname parts, return an empty array. + return partIndex < pathnameParts.length + ? pathnameParts.slice(partIndex).map((s) => encodeURIComponent(s)) + : [] + } + // Optional catchalls + case 'oc': { + // Optional catchalls receive all the remaining URL parts, unless this is + // the end of the pathname, in which case they return null. + return partIndex < pathnameParts.length + ? pathnameParts.slice(partIndex).map((s) => encodeURIComponent(s)) + : null + } + // Dynamic + case 'd': + case 'di': { + if (partIndex >= pathnameParts.length) { + // The route tree expected there to be more parts in the URL than there + // actually are. This could happen if the x-nextjs-rewritten-path header + // is incorrectly set, or potentially due to bug in Next.js. TODO: + // Should this be a hard error? During a prefetch, we can just abort. + // During a client navigation, we could trigger a hard refresh. But if + // it happens during initial render, we don't really have any + // recovery options. + return '' + } + return encodeURIComponent(pathnameParts[partIndex]) + } + default: + paramType satisfies never + return '' + } +} + +export function doesStaticSegmentAppearInURL(segment: string): boolean { + // This is not a parameterized segment; however, we need to determine + // whether or not this segment appears in the URL. For example, this route + // groups do not appear in the URL, so they should be skipped. Any other + // special cases must be handled here. + // TODO: Consider encoding this directly into the router tree instead of + // inferring it on the client based on the segment type. Something like + // a `doesAppearInURL` flag in FlightRouterState. + if ( + segment === ROOT_SEGMENT_KEY || + // For some reason, the loader tree sometimes includes extra __PAGE__ + // "layouts" when part of a parallel route. But it's not a leaf node. + // Otherwise, we wouldn't need this special case because pages are + // always leaf nodes. + // TODO: Investigate why the loader produces these fake page segments. + segment.startsWith(PAGE_SEGMENT_KEY) || + // Route groups. + (segment[0] === '(' && segment.endsWith(')')) + ) { + return false + } else { + // All other segment types appear in the URL + return true + } +} + +export function getCacheKeyForDynamicParam( + paramValue: RouteParamValue +): string { + // This needs to match the logic in get-dynamic-param.ts, until we're able to + // unify the various implementations so that these are always computed on + // the client. + return typeof paramValue === 'string' + ? paramValue + : paramValue === null + ? '' + : paramValue.join('/') +} diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.development.js index d709dbb0665e08..de82b44800c6b5 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.development.js @@ -31128,11 +31128,11 @@ }; (function () { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); })(); ("function" === typeof Map && @@ -31169,10 +31169,10 @@ !(function () { var internals = { bundleType: 1, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730" + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731" }; internals.overrideHookState = overrideHookState; internals.overrideHookStateDeletePath = overrideHookStateDeletePath; @@ -31318,7 +31318,7 @@ listenToAllSupportedEvents(container); return new ReactDOMHydrationRoot(initialChildren); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.production.js index 1f538fb83afbbc..7f47371a589283 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-client.production.js @@ -19231,14 +19231,14 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) { }; var isomorphicReactPackageVersion$jscomp$inline_2167 = React.version; if ( - "19.2.0-experimental-9784cb37-20250730" !== + "19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_2167 ) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion$jscomp$inline_2167, - "19.2.0-experimental-9784cb37-20250730" + "19.2.0-experimental-c260b38d-20250731" ) ); ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { @@ -19260,10 +19260,10 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { }; var internals$jscomp$inline_2852 = { bundleType: 0, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730" + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731" }; if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { var hook$jscomp$inline_2853 = __REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -19370,4 +19370,4 @@ exports.hydrateRoot = function (container, initialChildren, options) { listenToAllSupportedEvents(container); return new ReactDOMHydrationRoot(initialChildren); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.development.js index e4ed206e3e98ec..bf4cd227b9f152 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.development.js @@ -31180,11 +31180,11 @@ }; (function () { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); })(); ("function" === typeof Map && @@ -31221,10 +31221,10 @@ !(function () { var internals = { bundleType: 1, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730" + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731" }; internals.overrideHookState = overrideHookState; internals.overrideHookStateDeletePath = overrideHookStateDeletePath; @@ -31700,7 +31700,7 @@ exports.useFormStatus = function () { return resolveDispatcher().useHostTransitionStatus(); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.profiling.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.profiling.js index 7c545131e960e5..37c4866de46605 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.profiling.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-profiling.profiling.js @@ -20914,14 +20914,14 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) { }; var isomorphicReactPackageVersion$jscomp$inline_2389 = React.version; if ( - "19.2.0-experimental-9784cb37-20250730" !== + "19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_2389 ) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion$jscomp$inline_2389, - "19.2.0-experimental-9784cb37-20250730" + "19.2.0-experimental-c260b38d-20250731" ) ); ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { @@ -20943,10 +20943,10 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { }; var internals$jscomp$inline_3076 = { bundleType: 0, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730" + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731" }; if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { var hook$jscomp$inline_3077 = __REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -21213,7 +21213,7 @@ exports.useFormState = function (action, initialState, permalink) { exports.useFormStatus = function () { return ReactSharedInternals.H.useHostTransitionStatus(); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.development.js index e8f82b3a2e86e1..3ad47088685b2c 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.development.js @@ -4613,6 +4613,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5143,7 +5146,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6243,7 +6246,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7740,7 +7743,7 @@ (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request @@ -8132,6 +8135,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -8164,17 +8168,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -8287,7 +8291,7 @@ destination.push(endSuspenseBoundary) ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -8305,7 +8309,7 @@ hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -8352,7 +8356,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -10519,5 +10523,5 @@ 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.production.js index 587b298f058e26..0f5a9f31c96b80 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.browser.production.js @@ -3853,6 +3853,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4219,7 +4222,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -4955,7 +4958,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && finishSuspenseListRow(request, prevRow$jscomp$0); @@ -6089,7 +6095,7 @@ function finishedTask(request, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request), boundary.fallbackAbortableTasks.clear(), null !== row && @@ -6414,6 +6420,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error(formatProdErrorMessage(391)); return preparePreambleFromSubtree( @@ -6443,17 +6450,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6537,7 +6543,7 @@ function flushSegment(request, destination, segment, hoistableState) { destination.push("\x3c!--/$--\x3e") ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -6555,7 +6561,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -6594,7 +6600,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -7136,4 +7142,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.development.js index 18c5a02359e369..0cba1a18510cd2 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.development.js @@ -4613,6 +4613,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5143,7 +5146,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6243,7 +6246,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7740,7 +7743,7 @@ (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request @@ -8132,6 +8135,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -8164,17 +8168,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -8287,7 +8291,7 @@ destination.push(endSuspenseBoundary) ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -8305,7 +8309,7 @@ hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -8352,7 +8356,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -10519,5 +10523,5 @@ 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server' ); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.production.js index 16fcc5f14ce938..72528023ba214c 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server-legacy.node.production.js @@ -3904,6 +3904,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4270,7 +4273,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5006,7 +5009,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && finishSuspenseListRow(request, prevRow$jscomp$0); @@ -6168,7 +6174,7 @@ function finishedTask(request, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request), boundary.fallbackAbortableTasks.clear(), null !== row && @@ -6495,6 +6501,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -6527,17 +6534,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6623,7 +6629,7 @@ function flushSegment(request, destination, segment, hoistableState) { destination.push("\x3c!--/$--\x3e") ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -6641,7 +6647,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -6683,7 +6689,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -7239,4 +7245,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server' ); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.development.js index 0d12169707b187..cecd34d7c32a38 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.development.js @@ -4824,6 +4824,9 @@ ((ReactSharedInternals.recentlyCreatedOwnerStacks = 0), (lastResetTime = now)); } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5516,7 +5519,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6664,7 +6667,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -8181,7 +8184,7 @@ (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request @@ -8575,6 +8578,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -8607,17 +8611,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -8730,7 +8734,7 @@ hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -8747,7 +8751,7 @@ hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -8791,7 +8795,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -9508,11 +9512,11 @@ } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } var React = require("next/dist/compiled/react-experimental"), @@ -11331,5 +11335,5 @@ startWork(request); }); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.production.js index 20b39162dd6f89..b55d8006f290d3 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.browser.production.js @@ -4299,6 +4299,9 @@ function getViewTransitionClassName(defaultClass, eventClass) { ? null : eventClass; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4824,7 +4827,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5609,7 +5612,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && finishSuspenseListRow(request, prevRow$jscomp$0); @@ -6763,7 +6769,7 @@ function finishedTask(request, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request), boundary.fallbackAbortableTasks.clear(), null !== row && @@ -7090,6 +7096,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error(formatProdErrorMessage(391)); return preparePreambleFromSubtree( @@ -7119,17 +7126,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7206,7 +7212,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -7222,7 +7228,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -7258,7 +7264,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -7799,12 +7805,12 @@ function getPostponedState(request) { } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion, - "19.2.0-experimental-9784cb37-20250730" + "19.2.0-experimental-c260b38d-20250731" ) ); } @@ -8059,4 +8065,4 @@ exports.resumeAndPrerender = function (children, postponedState, options) { startWork(request); }); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.bun.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.bun.production.js index 0453dcdc145f13..ec93175e82aa02 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.bun.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.bun.production.js @@ -3922,6 +3922,9 @@ function getViewTransitionClassName(defaultClass, eventClass) { ? null : eventClass; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4303,7 +4306,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5100,7 +5103,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && finishSuspenseListRow(request, prevRow$jscomp$0); @@ -6282,7 +6288,7 @@ function finishedTask(request, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request), boundary.fallbackAbortableTasks.clear(), null !== row && @@ -6614,6 +6620,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -6646,17 +6653,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6731,7 +6737,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -6747,7 +6753,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); destination.write("\x3c!--$--\x3e"); @@ -6786,7 +6792,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -7287,13 +7293,13 @@ function addToReplayParent(node, parentKeyPath, trackedPostpones) { } var isomorphicReactPackageVersion$jscomp$inline_869 = React.version; if ( - "19.2.0-experimental-9784cb37-20250730" !== + "19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_869 ) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion$jscomp$inline_869 + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); exports.renderToReadableStream = function (children, options) { return new Promise(function (resolve, reject) { @@ -7384,4 +7390,4 @@ exports.renderToReadableStream = function (children, options) { startWork(request); }); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.development.js index d9bcaee3dda67f..e184a1dbb22c9b 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.development.js @@ -4832,6 +4832,9 @@ ((ReactSharedInternals.recentlyCreatedOwnerStacks = 0), (lastResetTime = now)); } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5532,7 +5535,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6680,7 +6683,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -8197,7 +8200,7 @@ (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request @@ -8591,6 +8594,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -8623,17 +8627,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -8746,7 +8750,7 @@ hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -8763,7 +8767,7 @@ hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -8807,7 +8811,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -9537,11 +9541,11 @@ } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } var React = require("next/dist/compiled/react-experimental"), @@ -11356,5 +11360,5 @@ startWork(request); }); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.production.js index 2c4498b483bb5d..66479e2ef23d35 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.edge.production.js @@ -4351,6 +4351,9 @@ function getViewTransitionClassName(defaultClass, eventClass) { ? null : eventClass; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4884,7 +4887,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5669,7 +5672,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && finishSuspenseListRow(request, prevRow$jscomp$0); @@ -6851,7 +6857,7 @@ function finishedTask(request, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request), boundary.fallbackAbortableTasks.clear(), null !== row && @@ -7180,6 +7186,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7212,17 +7219,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7301,7 +7307,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -7317,7 +7323,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -7356,7 +7362,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -7917,11 +7923,11 @@ function getPostponedState(request) { } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } ensureCorrectIsomorphicReactVersion(); @@ -8175,4 +8181,4 @@ exports.resumeAndPrerender = function (children, postponedState, options) { startWork(request); }); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.development.js index 74d6516577619b..447184321ce780 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.development.js @@ -4721,6 +4721,9 @@ ((ReactSharedInternals.recentlyCreatedOwnerStacks = 0), (lastResetTime = now)); } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5417,7 +5420,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6565,7 +6568,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -8082,7 +8085,7 @@ (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request @@ -8476,6 +8479,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -8508,17 +8512,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -8622,7 +8626,7 @@ hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -8639,7 +8643,7 @@ hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -8683,7 +8687,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -9398,11 +9402,11 @@ } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } function createDrainHandler(destination, request) { @@ -11527,5 +11531,5 @@ } }; }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.production.js index d6ce52e5a65f3f..9b6c28a30391dd 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-server.node.production.js @@ -4239,6 +4239,9 @@ function getViewTransitionClassName(defaultClass, eventClass) { ? null : eventClass; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4769,7 +4772,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5554,7 +5557,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && finishSuspenseListRow(request, prevRow$jscomp$0); @@ -6736,7 +6742,7 @@ function finishedTask(request, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request, boundary) || (boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request), boundary.fallbackAbortableTasks.clear(), null !== row && @@ -7065,6 +7071,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7097,17 +7104,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7186,7 +7192,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -7202,7 +7208,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -7241,7 +7247,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -7797,11 +7803,11 @@ function getPostponedState(request) { } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } ensureCorrectIsomorphicReactVersion(); @@ -8359,4 +8365,4 @@ exports.resumeToPipeableStream = function (children, postponedState, options) { } }; }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.development.js index 965ed567070833..9ac1476ccd82c4 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.development.js @@ -31449,11 +31449,11 @@ }; (function () { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-experimental-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-experimental-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-experimental-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); })(); ("function" === typeof Map && @@ -31490,10 +31490,10 @@ !(function () { var internals = { bundleType: 1, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730" + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731" }; internals.overrideHookState = overrideHookState; internals.overrideHookStateDeletePath = overrideHookStateDeletePath; @@ -31805,5 +31805,5 @@ } }; }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.production.js index ad2954012ebd8e..69071a589bd53b 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom-unstable_testing.production.js @@ -19547,14 +19547,14 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) { }; var isomorphicReactPackageVersion$jscomp$inline_2196 = React.version; if ( - "19.2.0-experimental-9784cb37-20250730" !== + "19.2.0-experimental-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_2196 ) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion$jscomp$inline_2196, - "19.2.0-experimental-9784cb37-20250730" + "19.2.0-experimental-c260b38d-20250731" ) ); ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { @@ -19576,10 +19576,10 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { }; var internals$jscomp$inline_2886 = { bundleType: 0, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730" + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731" }; if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { var hook$jscomp$inline_2887 = __REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -19837,4 +19837,4 @@ exports.observeVisibleRects = function ( } }; }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.development.js index 200ec40362ba93..270e4530dc9d5e 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.development.js @@ -416,7 +416,7 @@ exports.useFormStatus = function () { return resolveDispatcher().useHostTransitionStatus(); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.production.js index 43a93ba37d03a4..f49c6be5563c9b 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.production.js @@ -207,4 +207,4 @@ exports.useFormState = function (action, initialState, permalink) { exports.useFormStatus = function () { return ReactSharedInternals.H.useHostTransitionStatus(); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.development.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.development.js index eb4714bbdde8fa..ab68900166bda1 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.development.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.development.js @@ -336,5 +336,5 @@ })) : Internals.d.m(href)); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.production.js b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.production.js index 80b8cd797ddd91..4c986e249174a3 100644 --- a/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.production.js +++ b/packages/next/src/compiled/react-dom-experimental/cjs/react-dom.react-server.production.js @@ -149,4 +149,4 @@ exports.preloadModule = function (href, options) { }); } else Internals.d.m(href); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom-experimental/package.json b/packages/next/src/compiled/react-dom-experimental/package.json index 47bb11344702c0..fe0b646a8355bf 100644 --- a/packages/next/src/compiled/react-dom-experimental/package.json +++ b/packages/next/src/compiled/react-dom-experimental/package.json @@ -72,10 +72,10 @@ "./package.json": "./package.json" }, "dependencies": { - "scheduler": "0.0.0-experimental-9784cb37-20250730" + "scheduler": "0.0.0-experimental-c260b38d-20250731" }, "peerDependencies": { - "react": "0.0.0-experimental-9784cb37-20250730" + "react": "0.0.0-experimental-c260b38d-20250731" }, "browser": { "./server.js": "./server.browser.js", diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-client.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-client.development.js index d16c92114dbd92..4564247e398f66 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-client.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-client.development.js @@ -25519,11 +25519,11 @@ }; (function () { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); })(); ("function" === typeof Map && @@ -25560,10 +25560,10 @@ !(function () { var internals = { bundleType: 1, - version: "19.2.0-canary-9784cb37-20250730", + version: "19.2.0-canary-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-canary-9784cb37-20250730" + reconcilerVersion: "19.2.0-canary-c260b38d-20250731" }; internals.overrideHookState = overrideHookState; internals.overrideHookStateDeletePath = overrideHookStateDeletePath; @@ -25701,7 +25701,7 @@ listenToAllSupportedEvents(container); return new ReactDOMHydrationRoot(initialChildren); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-client.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-client.production.js index 77c80b6f150f8d..45282710e27161 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-client.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-client.production.js @@ -15695,14 +15695,14 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) { }; var isomorphicReactPackageVersion$jscomp$inline_1836 = React.version; if ( - "19.2.0-canary-9784cb37-20250730" !== + "19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_1836 ) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion$jscomp$inline_1836, - "19.2.0-canary-9784cb37-20250730" + "19.2.0-canary-c260b38d-20250731" ) ); ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { @@ -15724,10 +15724,10 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { }; var internals$jscomp$inline_2329 = { bundleType: 0, - version: "19.2.0-canary-9784cb37-20250730", + version: "19.2.0-canary-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-canary-9784cb37-20250730" + reconcilerVersion: "19.2.0-canary-c260b38d-20250731" }; if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { var hook$jscomp$inline_2330 = __REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -15825,4 +15825,4 @@ exports.hydrateRoot = function (container, initialChildren, options) { listenToAllSupportedEvents(container); return new ReactDOMHydrationRoot(initialChildren); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.development.js index f55eea982f2f5a..7a9118463fc88c 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.development.js @@ -25571,11 +25571,11 @@ }; (function () { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); })(); ("function" === typeof Map && @@ -25612,10 +25612,10 @@ !(function () { var internals = { bundleType: 1, - version: "19.2.0-canary-9784cb37-20250730", + version: "19.2.0-canary-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-canary-9784cb37-20250730" + reconcilerVersion: "19.2.0-canary-c260b38d-20250731" }; internals.overrideHookState = overrideHookState; internals.overrideHookStateDeletePath = overrideHookStateDeletePath; @@ -26083,7 +26083,7 @@ exports.useFormStatus = function () { return resolveDispatcher().useHostTransitionStatus(); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.profiling.js b/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.profiling.js index 484c4994687537..0dbb11a6f455d6 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.profiling.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-profiling.profiling.js @@ -16396,14 +16396,14 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) { }; var isomorphicReactPackageVersion$jscomp$inline_1940 = React.version; if ( - "19.2.0-canary-9784cb37-20250730" !== + "19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_1940 ) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion$jscomp$inline_1940, - "19.2.0-canary-9784cb37-20250730" + "19.2.0-canary-c260b38d-20250731" ) ); ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { @@ -16425,10 +16425,10 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) { }; var internals$jscomp$inline_1947 = { bundleType: 0, - version: "19.2.0-canary-9784cb37-20250730", + version: "19.2.0-canary-c260b38d-20250731", rendererPackageName: "react-dom", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-canary-9784cb37-20250730", + reconcilerVersion: "19.2.0-canary-c260b38d-20250731", getLaneLabelMap: function () { for ( var map = new Map(), lane = 1, index$281 = 0; @@ -16701,7 +16701,7 @@ exports.useFormState = function (action, initialState, permalink) { exports.useFormStatus = function () { return ReactSharedInternals.H.useHostTransitionStatus(); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.development.js index f6576bdbb7cbab..bf46dbb087f4d1 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.development.js @@ -4353,6 +4353,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4877,7 +4880,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5911,7 +5914,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7116,7 +7119,7 @@ (row = boundary$jscomp$0.row), null !== row && hoistHoistables(row.hoistables, boundary$jscomp$0.contentState), - 500 < boundary$jscomp$0.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary$jscomp$0) || (boundary$jscomp$0.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -7479,6 +7482,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7511,17 +7515,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7634,7 +7638,7 @@ destination.push(endSuspenseBoundary) ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -7652,7 +7656,7 @@ hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -7699,7 +7703,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -9734,5 +9738,5 @@ 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.production.js index 6bc759740d5942..0b2bd7d9fa4712 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.browser.production.js @@ -3619,6 +3619,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -3981,7 +3984,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -4595,7 +4598,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow && 0 === --prevRow.pendingTasks && finishSuspenseListRow(request, prevRow); @@ -5505,7 +5511,7 @@ function finishedTask(request$jscomp$0, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -5820,6 +5826,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error(formatProdErrorMessage(391)); return preparePreambleFromSubtree( @@ -5849,17 +5856,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -5943,7 +5949,7 @@ function flushSegment(request, destination, segment, hoistableState) { destination.push("\x3c!--/$--\x3e") ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -5961,7 +5967,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -6000,7 +6006,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -6485,4 +6491,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.development.js index bf26e024310878..cacd1d11389ef5 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.development.js @@ -4353,6 +4353,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4877,7 +4880,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5911,7 +5914,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7116,7 +7119,7 @@ (row = boundary$jscomp$0.row), null !== row && hoistHoistables(row.hoistables, boundary$jscomp$0.contentState), - 500 < boundary$jscomp$0.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary$jscomp$0) || (boundary$jscomp$0.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -7479,6 +7482,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7511,17 +7515,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7634,7 +7638,7 @@ destination.push(endSuspenseBoundary) ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -7652,7 +7656,7 @@ hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -7699,7 +7703,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -9734,5 +9738,5 @@ 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server' ); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.production.js index 793916f78188d1..2ad6974f253088 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server-legacy.node.production.js @@ -3666,6 +3666,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4028,7 +4031,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -4642,7 +4645,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow && 0 === --prevRow.pendingTasks && finishSuspenseListRow(request, prevRow); @@ -5570,7 +5576,7 @@ function finishedTask(request$jscomp$0, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -5890,6 +5896,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -5922,17 +5929,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6018,7 +6024,7 @@ function flushSegment(request, destination, segment, hoistableState) { destination.push("\x3c!--/$--\x3e") ); if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) return ( @@ -6036,7 +6042,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); request.renderState.generateStaticMarkup || @@ -6078,7 +6084,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -6568,4 +6574,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server' ); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.development.js index 01aeb021663745..1a07046ab3be47 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.development.js @@ -4507,6 +4507,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5081,7 +5084,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6114,7 +6117,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7339,7 +7342,7 @@ (row = boundary$jscomp$0.row), null !== row && hoistHoistables(row.hoistables, boundary$jscomp$0.contentState), - 500 < boundary$jscomp$0.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary$jscomp$0) || (boundary$jscomp$0.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -7704,6 +7707,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7736,17 +7740,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7859,7 +7863,7 @@ hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -7876,7 +7880,7 @@ hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -7920,7 +7924,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -8481,11 +8485,11 @@ } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } var React = require("next/dist/compiled/react"), @@ -10175,5 +10179,5 @@ startWork(request); }); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.production.js index b1ee86ccd3ec15..25f7285013f1db 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.browser.production.js @@ -4002,6 +4002,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4413,7 +4416,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5026,7 +5029,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow && 0 === --prevRow.pendingTasks && finishSuspenseListRow(request, prevRow); @@ -5957,7 +5963,7 @@ function finishedTask(request$jscomp$0, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -6270,6 +6276,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error(formatProdErrorMessage(391)); return preparePreambleFromSubtree( @@ -6299,17 +6306,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6386,7 +6392,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -6402,7 +6408,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -6438,7 +6444,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -6869,12 +6875,12 @@ function addToReplayParent(node, parentKeyPath, trackedPostpones) { } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( formatProdErrorMessage( 527, isomorphicReactPackageVersion, - "19.2.0-canary-9784cb37-20250730" + "19.2.0-canary-c260b38d-20250731" ) ); } @@ -7021,4 +7027,4 @@ exports.renderToReadableStream = function (children, options) { startWork(request); }); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.bun.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.bun.production.js index dfd672ab7d18d8..2c5c337521a7ea 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.bun.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.bun.production.js @@ -3646,6 +3646,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4023,7 +4026,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -4648,7 +4651,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow && 0 === --prevRow.pendingTasks && finishSuspenseListRow(request, prevRow); @@ -5597,7 +5603,7 @@ function finishedTask(request$jscomp$0, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -5918,6 +5924,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -5950,17 +5957,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6035,7 +6041,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -6051,7 +6057,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); destination.write("\x3c!--$--\x3e"); @@ -6090,7 +6096,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -6517,13 +6523,13 @@ function addToReplayParent(node, parentKeyPath, trackedPostpones) { } var isomorphicReactPackageVersion$jscomp$inline_816 = React.version; if ( - "19.2.0-canary-9784cb37-20250730" !== + "19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion$jscomp$inline_816 ) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion$jscomp$inline_816 + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); exports.renderToReadableStream = function (children, options) { return new Promise(function (resolve, reject) { @@ -6614,4 +6620,4 @@ exports.renderToReadableStream = function (children, options) { startWork(request); }); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.development.js index d359dc312bfee9..6d8fa69a8fbc54 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.development.js @@ -4509,6 +4509,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -5091,7 +5094,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6124,7 +6127,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7349,7 +7352,7 @@ (row = boundary$jscomp$0.row), null !== row && hoistHoistables(row.hoistables, boundary$jscomp$0.contentState), - 500 < boundary$jscomp$0.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary$jscomp$0) || (boundary$jscomp$0.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -7714,6 +7717,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7746,17 +7750,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7869,7 +7873,7 @@ hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -7886,7 +7890,7 @@ hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -7930,7 +7934,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -8504,11 +8508,11 @@ } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } var React = require("next/dist/compiled/react"), @@ -10194,5 +10198,5 @@ startWork(request); }); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.production.js index c4e59efaa8cb8d..cbde05a36275f3 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.edge.production.js @@ -4046,6 +4046,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4465,7 +4468,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -5078,7 +5081,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow && 0 === --prevRow.pendingTasks && finishSuspenseListRow(request, prevRow); @@ -6027,7 +6033,7 @@ function finishedTask(request$jscomp$0, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -6345,6 +6351,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -6377,17 +6384,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6466,7 +6472,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -6482,7 +6488,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -6521,7 +6527,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -6967,11 +6973,11 @@ function addToReplayParent(node, parentKeyPath, trackedPostpones) { } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } ensureCorrectIsomorphicReactVersion(); @@ -7117,4 +7123,4 @@ exports.renderToReadableStream = function (children, options) { startWork(request); }); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.development.js index b407285f8f4c01..fbfeff6cec5aad 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.development.js @@ -4412,6 +4412,9 @@ } return ""; } + function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; + } function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4990,7 +4993,7 @@ if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -6023,7 +6026,7 @@ ) { if ( ((newBoundary.status = COMPLETED), - !(500 < newBoundary.byteSize)) + !isEligibleForOutlining(request, newBoundary)) ) { null !== prevRow$jscomp$0 && 0 === --prevRow$jscomp$0.pendingTasks && @@ -7248,7 +7251,7 @@ (row = boundary$jscomp$0.row), null !== row && hoistHoistables(row.hoistables, boundary$jscomp$0.contentState), - 500 < boundary$jscomp$0.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary$jscomp$0) || (boundary$jscomp$0.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -7613,6 +7616,7 @@ switch (boundary.status) { case COMPLETED: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -7645,17 +7649,17 @@ null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || + (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -7759,7 +7763,7 @@ hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -7776,7 +7780,7 @@ hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -7820,7 +7824,7 @@ completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -8379,11 +8383,11 @@ } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } function createDrainHandler(destination, request) { @@ -10254,5 +10258,5 @@ startWork(request); }); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.production.js index 8b0fbfb7b23a1b..7d460beaa3fad7 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom-server.node.production.js @@ -3949,6 +3949,9 @@ function describeComponentStackByType(type) { } return ""; } +function isEligibleForOutlining(request, boundary) { + return 500 < boundary.byteSize && null === boundary.contentPreamble; +} function defaultErrorHandler(error) { if ( "object" === typeof error && @@ -4365,7 +4368,7 @@ function tryToResolveTogetherRow(request, togetherRow) { if ( 1 !== rowBoundary.pendingTasks || rowBoundary.parentFlushed || - 500 < rowBoundary.byteSize + isEligibleForOutlining(request, rowBoundary) ) { allCompleteAndInlinable = !1; break; @@ -4978,7 +4981,10 @@ function renderElement(request, task, keyPath, type, props, ref) { queueCompletedSegment(newBoundary, contentRootSegment), 0 === newBoundary.pendingTasks && 0 === newBoundary.status) ) { - if (((newBoundary.status = 1), !(500 < newBoundary.byteSize))) { + if ( + ((newBoundary.status = 1), + !isEligibleForOutlining(request, newBoundary)) + ) { null !== prevRow && 0 === --prevRow.pendingTasks && finishSuspenseListRow(request, prevRow); @@ -5927,7 +5933,7 @@ function finishedTask(request$jscomp$0, boundary, row, segment) { (row = boundary.row), null !== row && hoistHoistables(row.hoistables, boundary.contentState), - 500 < boundary.byteSize || + isEligibleForOutlining(request$jscomp$0, boundary) || (boundary.fallbackAbortableTasks.forEach( abortTaskSoft, request$jscomp$0 @@ -6245,6 +6251,7 @@ function preparePreambleFromSegment( switch (boundary.status) { case 1: hoistPreambleState(request.renderState, preamble); + request.byteSize += boundary.byteSize; segment = boundary.completedSegments[0]; if (!segment) throw Error( @@ -6277,17 +6284,16 @@ function preparePreamble(request) { null === request.completedPreambleSegments ) { var collectedPreambleSegments = [], + originalRequestByteSize = request.byteSize, hasPendingPreambles = preparePreambleFromSegment( request, request.completedRootSegment, collectedPreambleSegments ), preamble = request.renderState.preamble; - if ( - !1 === hasPendingPreambles || - (preamble.headChunks && preamble.bodyChunks) - ) - request.completedPreambleSegments = collectedPreambleSegments; + !1 === hasPendingPreambles || (preamble.headChunks && preamble.bodyChunks) + ? (request.completedPreambleSegments = collectedPreambleSegments) + : (request.byteSize = originalRequestByteSize); } } function flushSubtree(request, destination, segment, hoistableState) { @@ -6366,7 +6372,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.fallbackState), flushSubtree(request, destination, segment, hoistableState); else if ( - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && flushedByteSize + boundary.byteSize > request.progressiveChunkSize ) (boundary.rootSegmentID = request.nextSegmentId++), @@ -6382,7 +6388,7 @@ function flushSegment(request, destination, segment, hoistableState) { hoistableState && hoistHoistables(hoistableState, boundary.contentState); segment = boundary.row; null !== segment && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --segment.pendingTasks && finishSuspenseListRow(request, segment); writeChunkAndReturn(destination, startCompletedSuspenseBoundary); @@ -6421,7 +6427,7 @@ function flushCompletedBoundary(request, destination, boundary) { completedSegments.length = 0; completedSegments = boundary.row; null !== completedSegments && - 500 < boundary.byteSize && + isEligibleForOutlining(request, boundary) && 0 === --completedSegments.pendingTasks && finishSuspenseListRow(request, completedSegments); writeHoistablesForBoundary( @@ -6859,11 +6865,11 @@ function addToReplayParent(node, parentKeyPath, trackedPostpones) { } function ensureCorrectIsomorphicReactVersion() { var isomorphicReactPackageVersion = React.version; - if ("19.2.0-canary-9784cb37-20250730" !== isomorphicReactPackageVersion) + if ("19.2.0-canary-c260b38d-20250731" !== isomorphicReactPackageVersion) throw Error( 'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + - "\n - react-dom: 19.2.0-canary-9784cb37-20250730\nLearn more: https://react.dev/warnings/version-mismatch") + "\n - react-dom: 19.2.0-canary-c260b38d-20250731\nLearn more: https://react.dev/warnings/version-mismatch") ); } ensureCorrectIsomorphicReactVersion(); @@ -7201,4 +7207,4 @@ exports.renderToReadableStream = function (children, options) { startWork(request); }); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom.development.js index 966771f89dd35a..d47b737f546f97 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom.development.js @@ -416,7 +416,7 @@ exports.useFormStatus = function () { return resolveDispatcher().useHostTransitionStatus(); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom.production.js index 3283106070cb5d..c0496a4b4f86c8 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom.production.js @@ -207,4 +207,4 @@ exports.useFormState = function (action, initialState, permalink) { exports.useFormStatus = function () { return ReactSharedInternals.H.useHostTransitionStatus(); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.development.js b/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.development.js index e33f8127069be4..ff10f4abd1cf19 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.development.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.development.js @@ -336,5 +336,5 @@ })) : Internals.d.m(href)); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.production.js b/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.production.js index d8146574a42c34..f4ac877f73c372 100644 --- a/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.production.js +++ b/packages/next/src/compiled/react-dom/cjs/react-dom.react-server.production.js @@ -149,4 +149,4 @@ exports.preloadModule = function (href, options) { }); } else Internals.d.m(href); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-dom/package.json b/packages/next/src/compiled/react-dom/package.json index 65b84a8d99498a..29c7ea06ef75bc 100644 --- a/packages/next/src/compiled/react-dom/package.json +++ b/packages/next/src/compiled/react-dom/package.json @@ -67,10 +67,10 @@ "./package.json": "./package.json" }, "dependencies": { - "scheduler": "0.27.0-canary-9784cb37-20250730" + "scheduler": "0.27.0-canary-c260b38d-20250731" }, "peerDependencies": { - "react": "19.2.0-canary-9784cb37-20250730" + "react": "19.2.0-canary-c260b38d-20250731" }, "browser": { "./server.js": "./server.browser.js", diff --git a/packages/next/src/compiled/react-experimental/cjs/react.development.js b/packages/next/src/compiled/react-experimental/cjs/react.development.js index 82c340196458b3..2d5cfddeda011c 100644 --- a/packages/next/src/compiled/react-experimental/cjs/react.development.js +++ b/packages/next/src/compiled/react-experimental/cjs/react.development.js @@ -1328,7 +1328,7 @@ exports.useTransition = function () { return resolveDispatcher().useTransition(); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react-experimental/cjs/react.production.js b/packages/next/src/compiled/react-experimental/cjs/react.production.js index 71a5575fcd4d41..62a619797b636f 100644 --- a/packages/next/src/compiled/react-experimental/cjs/react.production.js +++ b/packages/next/src/compiled/react-experimental/cjs/react.production.js @@ -605,4 +605,4 @@ exports.useSyncExternalStore = function ( exports.useTransition = function () { return ReactSharedInternals.H.useTransition(); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-experimental/cjs/react.react-server.development.js b/packages/next/src/compiled/react-experimental/cjs/react.react-server.development.js index 986800983468a8..6db63726ef072e 100644 --- a/packages/next/src/compiled/react-experimental/cjs/react.react-server.development.js +++ b/packages/next/src/compiled/react-experimental/cjs/react.react-server.development.js @@ -996,5 +996,5 @@ exports.useMemo = function (create, deps) { return resolveDispatcher().useMemo(create, deps); }; - exports.version = "19.2.0-experimental-9784cb37-20250730"; + exports.version = "19.2.0-experimental-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react-experimental/cjs/react.react-server.production.js b/packages/next/src/compiled/react-experimental/cjs/react.react-server.production.js index 4ed29a8e71c685..849d5925a12339 100644 --- a/packages/next/src/compiled/react-experimental/cjs/react.react-server.production.js +++ b/packages/next/src/compiled/react-experimental/cjs/react.react-server.production.js @@ -572,4 +572,4 @@ exports.useId = function () { exports.useMemo = function (create, deps) { return ReactSharedInternals.H.useMemo(create, deps); }; -exports.version = "19.2.0-experimental-9784cb37-20250730"; +exports.version = "19.2.0-experimental-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react-is/package.json b/packages/next/src/compiled/react-is/package.json index 4864a01bec71c9..bf9586879a57a0 100644 --- a/packages/next/src/compiled/react-is/package.json +++ b/packages/next/src/compiled/react-is/package.json @@ -1,6 +1,6 @@ { "name": "react-is", - "version": "19.2.0-canary-9784cb37-20250730", + "version": "19.2.0-canary-c260b38d-20250731", "description": "Brand checking of React Elements.", "main": "index.js", "sideEffects": false, diff --git a/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-client.browser.development.js b/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-client.browser.development.js index 34d08d01a5c39d..b91a9c443febf5 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-client.browser.development.js +++ b/packages/next/src/compiled/react-server-dom-turbopack-experimental/cjs/react-server-dom-turbopack-client.browser.development.js @@ -4417,10 +4417,10 @@ return hook.checkDCE ? !0 : !1; })({ bundleType: 1, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-server-dom-turbopack", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730", + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731", getCurrentComponentInfo: function () { return currentOwnerInDEV; } diff --git a/packages/next/src/compiled/react-server-dom-turbopack-experimental/package.json b/packages/next/src/compiled/react-server-dom-turbopack-experimental/package.json index 60011651d3e8bd..798fb636d6c045 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack-experimental/package.json +++ b/packages/next/src/compiled/react-server-dom-turbopack-experimental/package.json @@ -48,7 +48,7 @@ "neo-async": "^2.6.1" }, "peerDependencies": { - "react": "0.0.0-experimental-9784cb37-20250730", - "react-dom": "0.0.0-experimental-9784cb37-20250730" + "react": "0.0.0-experimental-c260b38d-20250731", + "react-dom": "0.0.0-experimental-c260b38d-20250731" } } \ No newline at end of file diff --git a/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js b/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js index 442a602a93af03..21fb34d6e31dc5 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +++ b/packages/next/src/compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js @@ -3355,10 +3355,10 @@ return hook.checkDCE ? !0 : !1; })({ bundleType: 1, - version: "19.2.0-canary-9784cb37-20250730", + version: "19.2.0-canary-c260b38d-20250731", rendererPackageName: "react-server-dom-turbopack", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-canary-9784cb37-20250730", + reconcilerVersion: "19.2.0-canary-c260b38d-20250731", getCurrentComponentInfo: function () { return currentOwnerInDEV; } diff --git a/packages/next/src/compiled/react-server-dom-turbopack/package.json b/packages/next/src/compiled/react-server-dom-turbopack/package.json index 87f84a8b25b33c..7ca359c50c6f7e 100644 --- a/packages/next/src/compiled/react-server-dom-turbopack/package.json +++ b/packages/next/src/compiled/react-server-dom-turbopack/package.json @@ -48,7 +48,7 @@ "neo-async": "^2.6.1" }, "peerDependencies": { - "react": "19.2.0-canary-9784cb37-20250730", - "react-dom": "19.2.0-canary-9784cb37-20250730" + "react": "19.2.0-canary-c260b38d-20250731", + "react-dom": "19.2.0-canary-c260b38d-20250731" } } \ No newline at end of file diff --git a/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.browser.development.js b/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.browser.development.js index 1c1d0aa0d792e0..09dad897432f85 100644 --- a/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.browser.development.js +++ b/packages/next/src/compiled/react-server-dom-webpack-experimental/cjs/react-server-dom-webpack-client.browser.development.js @@ -4433,10 +4433,10 @@ return hook.checkDCE ? !0 : !1; })({ bundleType: 1, - version: "19.2.0-experimental-9784cb37-20250730", + version: "19.2.0-experimental-c260b38d-20250731", rendererPackageName: "react-server-dom-webpack", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-experimental-9784cb37-20250730", + reconcilerVersion: "19.2.0-experimental-c260b38d-20250731", getCurrentComponentInfo: function () { return currentOwnerInDEV; } diff --git a/packages/next/src/compiled/react-server-dom-webpack-experimental/package.json b/packages/next/src/compiled/react-server-dom-webpack-experimental/package.json index d1383bfe59af36..16612b8944e53d 100644 --- a/packages/next/src/compiled/react-server-dom-webpack-experimental/package.json +++ b/packages/next/src/compiled/react-server-dom-webpack-experimental/package.json @@ -64,8 +64,8 @@ "webpack-sources": "^3.2.0" }, "peerDependencies": { - "react": "0.0.0-experimental-9784cb37-20250730", - "react-dom": "0.0.0-experimental-9784cb37-20250730", + "react": "0.0.0-experimental-c260b38d-20250731", + "react-dom": "0.0.0-experimental-c260b38d-20250731", "webpack": "^5.59.0" } } \ No newline at end of file diff --git a/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js b/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js index fe500edb9399e6..51c5d967569022 100644 --- a/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +++ b/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js @@ -3371,10 +3371,10 @@ return hook.checkDCE ? !0 : !1; })({ bundleType: 1, - version: "19.2.0-canary-9784cb37-20250730", + version: "19.2.0-canary-c260b38d-20250731", rendererPackageName: "react-server-dom-webpack", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.0-canary-9784cb37-20250730", + reconcilerVersion: "19.2.0-canary-c260b38d-20250731", getCurrentComponentInfo: function () { return currentOwnerInDEV; } diff --git a/packages/next/src/compiled/react-server-dom-webpack/package.json b/packages/next/src/compiled/react-server-dom-webpack/package.json index 7468d205382bda..340de427837cf1 100644 --- a/packages/next/src/compiled/react-server-dom-webpack/package.json +++ b/packages/next/src/compiled/react-server-dom-webpack/package.json @@ -64,8 +64,8 @@ "webpack-sources": "^3.2.0" }, "peerDependencies": { - "react": "19.2.0-canary-9784cb37-20250730", - "react-dom": "19.2.0-canary-9784cb37-20250730", + "react": "19.2.0-canary-c260b38d-20250731", + "react-dom": "19.2.0-canary-c260b38d-20250731", "webpack": "^5.59.0" } } \ No newline at end of file diff --git a/packages/next/src/compiled/react/cjs/react.development.js b/packages/next/src/compiled/react/cjs/react.development.js index 1e44eee4dffc02..e33c44204d27c0 100644 --- a/packages/next/src/compiled/react/cjs/react.development.js +++ b/packages/next/src/compiled/react/cjs/react.development.js @@ -1244,7 +1244,7 @@ exports.useTransition = function () { return resolveDispatcher().useTransition(); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && diff --git a/packages/next/src/compiled/react/cjs/react.production.js b/packages/next/src/compiled/react/cjs/react.production.js index f50756e22263aa..7a5c5f39c2abdc 100644 --- a/packages/next/src/compiled/react/cjs/react.production.js +++ b/packages/next/src/compiled/react/cjs/react.production.js @@ -543,4 +543,4 @@ exports.useSyncExternalStore = function ( exports.useTransition = function () { return ReactSharedInternals.H.useTransition(); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/react/cjs/react.react-server.development.js b/packages/next/src/compiled/react/cjs/react.react-server.development.js index c6c190f6cfac7f..4e3b7808656a6a 100644 --- a/packages/next/src/compiled/react/cjs/react.react-server.development.js +++ b/packages/next/src/compiled/react/cjs/react.react-server.development.js @@ -816,5 +816,5 @@ exports.useMemo = function (create, deps) { return resolveDispatcher().useMemo(create, deps); }; - exports.version = "19.2.0-canary-9784cb37-20250730"; + exports.version = "19.2.0-canary-c260b38d-20250731"; })(); diff --git a/packages/next/src/compiled/react/cjs/react.react-server.production.js b/packages/next/src/compiled/react/cjs/react.react-server.production.js index 886310dd857c2c..82facd6982b4a7 100644 --- a/packages/next/src/compiled/react/cjs/react.react-server.production.js +++ b/packages/next/src/compiled/react/cjs/react.react-server.production.js @@ -430,4 +430,4 @@ exports.useId = function () { exports.useMemo = function (create, deps) { return ReactSharedInternals.H.useMemo(create, deps); }; -exports.version = "19.2.0-canary-9784cb37-20250730"; +exports.version = "19.2.0-canary-c260b38d-20250731"; diff --git a/packages/next/src/compiled/unistore/unistore.js b/packages/next/src/compiled/unistore/unistore.js index 1cdc5b2ecf91d7..3e50c847973f08 100644 --- a/packages/next/src/compiled/unistore/unistore.js +++ b/packages/next/src/compiled/unistore/unistore.js @@ -1 +1 @@ -(()=>{var t={822:t=>{function n(t,i){for(var _ in i)t[_]=i[_];return t}t.exports=function(t){var i=[];function u(t){for(var _=[],a=0;a{var t={430:t=>{function n(t,i){for(var _ in i)t[_]=i[_];return t}t.exports=function(t){var i=[];function u(t){for(var _=[],a=0;a { + const { workStore } = ctx + const renderOpts = ctx.renderOpts + + function onFlightDataRenderError(err: DigestedError) { + return renderOpts.onInstrumentationRequestError?.( + err, + req, + // TODO(runtime-ppr): should we use a different value? + createErrorContext(ctx, 'react-server-components-payload') + ) + } + const onError = createFlightReactServerErrorHandler( + false, + onFlightDataRenderError + ) + + const metadata: AppPageRenderResultMetadata = {} + + const generatePayload = () => generateDynamicRSCPayload(ctx, undefined) + + const { + componentMod: { tree }, + getDynamicParamFromSegment, + } = ctx + const rootParams = getRootParams(tree, getDynamicParamFromSegment) + + // We need to share caches between the prospective prerender and the final prerender, + // but we're not going to persist this anywhere. + const prerenderResumeDataCache = createPrerenderResumeDataCache() + // We're not resuming an existing render. + const renderResumeDataCache = null + + await prospectiveRuntimeServerPrerender( + ctx, + generatePayload, + prerenderResumeDataCache, + renderResumeDataCache, + rootParams, + requestStore.cookies, + requestStore.draftMode + ) + + const response = await finalRuntimeServerPrerender( + ctx, + generatePayload, + prerenderResumeDataCache, + renderResumeDataCache, + rootParams, + requestStore.cookies, + requestStore.draftMode, + onError + ) + + applyMetadataFromPrerenderResult(response, metadata, workStore) + metadata.fetchMetrics = ctx.workStore.fetchMetrics + + if (response.isPartial) { + res.setHeader(NEXT_DID_POSTPONE_HEADER, '1') + } + + return new FlightRenderResult(response.result.prelude, metadata) +} + +async function prospectiveRuntimeServerPrerender( + ctx: AppRenderContext, + getPayload: () => any, + prerenderResumeDataCache: PrerenderResumeDataCache | null, + renderResumeDataCache: RenderResumeDataCache | null, + rootParams: Params, + cookies: PrerenderStoreModernRuntime['cookies'], + draftMode: PrerenderStoreModernRuntime['draftMode'] +) { + const { implicitTags, renderOpts, workStore } = ctx + + const { clientReferenceManifest, ComponentMod } = renderOpts + + assertClientReferenceManifest(clientReferenceManifest) + + // Prerender controller represents the lifetime of the prerender. + // It will be aborted when a Task is complete or a synchronously aborting + // API is called. Notably during cache-filling renders this does not actually + // terminate the render itself which will continue until all caches are filled + const initialServerPrerenderController = new AbortController() + + // This controller represents the lifetime of the React render call. Notably + // during the cache-filling render it is different from the prerender controller + // because we don't want to end the react render until all caches are filled. + const initialServerRenderController = new AbortController() + + // The cacheSignal helps us track whether caches are still filling or we are ready + // to cut the render off. + const cacheSignal = new CacheSignal() + + const initialServerPrerenderStore: PrerenderStoreModernRuntime = { + type: 'prerender-runtime', + phase: 'render', + rootParams, + implicitTags, + renderSignal: initialServerRenderController.signal, + controller: initialServerPrerenderController, + // During the initial prerender we need to track all cache reads to ensure + // we render long enough to fill every cache it is possible to visit during + // the final prerender. + cacheSignal, + // We only need to track dynamic accesses during the final prerender. + dynamicTracking: null, + // Runtime prefetches are never cached server-side, only client-side, + // so we set `expire` and `revalidate` to their minimum values just in case. + revalidate: 1, + expire: 0, + stale: INFINITE_CACHE, + tags: [...implicitTags.tags], + renderResumeDataCache, + prerenderResumeDataCache, + hmrRefreshHash: undefined, + captureOwnerStack: undefined, + // These are not present in regular prerenders, but allowed in a runtime prerender. + cookies, + draftMode, + } + + // We're not going to use the result of this render because the only time it could be used + // is if it completes in a microtask and that's likely very rare for any non-trivial app + const initialServerPayload = await workUnitAsyncStorage.run( + initialServerPrerenderStore, + getPayload + ) + + const pendingInitialServerResult = workUnitAsyncStorage.run( + initialServerPrerenderStore, + ComponentMod.prerender, + initialServerPayload, + clientReferenceManifest.clientModules, + { + filterStackFrame, + onError: (err) => { + const digest = getDigestForWellKnownError(err) + + if (digest) { + return digest + } + + if (initialServerPrerenderController.signal.aborted) { + // The render aborted before this error was handled which indicates + // the error is caused by unfinished components within the render + return + } else if ( + process.env.NEXT_DEBUG_BUILD || + process.env.__NEXT_VERBOSE_LOGGING + ) { + printDebugThrownValueForProspectiveRender(err, workStore.route) + } + }, + // we don't care to track postpones during the prospective render because we need + // to always do a final render anyway + onPostpone: undefined, + // We don't want to stop rendering until the cacheSignal is complete so we pass + // a different signal to this render call than is used by dynamic APIs to signify + // transitioning out of the prerender environment + signal: initialServerRenderController.signal, + } + ) + + // Wait for all caches to be finished filling and for async imports to resolve + trackPendingModules(cacheSignal) + await cacheSignal.cacheReady() + + initialServerRenderController.abort() + initialServerPrerenderController.abort() + + // We don't need to continue the prerender process if we already + // detected invalid dynamic usage in the initial prerender phase. + if (workStore.invalidDynamicUsageError) { + throw workStore.invalidDynamicUsageError + } + + try { + return await createReactServerPrerenderResult(pendingInitialServerResult) + } catch (err) { + if ( + initialServerRenderController.signal.aborted || + initialServerPrerenderController.signal.aborted + ) { + // These are expected errors that might error the prerender. we ignore them. + } else if ( + process.env.NEXT_DEBUG_BUILD || + process.env.__NEXT_VERBOSE_LOGGING + ) { + // We don't normally log these errors because we are going to retry anyway but + // it can be useful for debugging Next.js itself to get visibility here when needed + printDebugThrownValueForProspectiveRender(err, workStore.route) + } + return null + } +} + +async function finalRuntimeServerPrerender( + ctx: AppRenderContext, + getPayload: () => any, + prerenderResumeDataCache: PrerenderResumeDataCache | null, + renderResumeDataCache: RenderResumeDataCache | null, + rootParams: Params, + cookies: PrerenderStoreModernRuntime['cookies'], + draftMode: PrerenderStoreModernRuntime['draftMode'], + onError: (err: unknown) => string | undefined +) { + const { implicitTags, renderOpts } = ctx + + const { + clientReferenceManifest, + ComponentMod, + experimental, + isDebugDynamicAccesses, + } = renderOpts + + assertClientReferenceManifest(clientReferenceManifest) + + const selectStaleTime = createSelectStaleTime(experimental) + + let serverIsDynamic = false + const finalServerController = new AbortController() + + const serverDynamicTracking = createDynamicTrackingState( + isDebugDynamicAccesses + ) + + const finalServerPrerenderStore: PrerenderStoreModernRuntime = { + type: 'prerender-runtime', + phase: 'render', + rootParams, + implicitTags, + renderSignal: finalServerController.signal, + controller: finalServerController, + // All caches we could read must already be filled so no tracking is necessary + cacheSignal: null, + dynamicTracking: serverDynamicTracking, + // Runtime prefetches are never cached server-side, only client-side, + // so we set `expire` and `revalidate` to their minimum values just in case. + revalidate: 1, + expire: 0, + stale: INFINITE_CACHE, + tags: [...implicitTags.tags], + prerenderResumeDataCache, + renderResumeDataCache, + hmrRefreshHash: undefined, + captureOwnerStack: undefined, + // These are not present in regular prerenders, but allowed in a runtime prerender. + cookies, + draftMode, + } + + const finalRSCPayload = await workUnitAsyncStorage.run( + finalServerPrerenderStore, + getPayload + ) + + let prerenderIsPending = true + const result = await prerenderAndAbortInSequentialTasks( + async () => { + const prerenderResult = await workUnitAsyncStorage.run( + finalServerPrerenderStore, + ComponentMod.prerender, + finalRSCPayload, + clientReferenceManifest.clientModules, + { + filterStackFrame, + onError, + signal: finalServerController.signal, + } + ) + prerenderIsPending = false + return prerenderResult + }, + () => { + if (finalServerController.signal.aborted) { + // If the server controller is already aborted we must have called something + // that required aborting the prerender synchronously such as with new Date() + serverIsDynamic = true + return + } + + if (prerenderIsPending) { + // If prerenderIsPending then we have blocked for longer than a Task and we assume + // there is something unfinished. + serverIsDynamic = true + } + finalServerController.abort() + } + ) + + warnOnSyncDynamicError(serverDynamicTracking) + + return { + result, + // TODO(runtime-ppr): do we need to produce a digest map here? + // digestErrorsMap: ..., + dynamicAccess: serverDynamicTracking, + isPartial: serverIsDynamic, + collectedRevalidate: finalServerPrerenderStore.revalidate, + collectedExpire: finalServerPrerenderStore.expire, + collectedStale: selectStaleTime(finalServerPrerenderStore.stale), + collectedTags: finalServerPrerenderStore.tags, + } +} + /** * Performs a "warmup" render of the RSC payload for a given route. This function is called by the server * prior to an actual render request in Dev mode only. It's purpose is to fill caches so the actual render @@ -1190,6 +1517,7 @@ async function renderToHTMLOrFlightImpl( switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'cache': case 'private-cache': return true @@ -1299,6 +1627,7 @@ async function renderToHTMLOrFlightImpl( const { flightRouterState, isPrefetchRequest, + isRuntimePrefetchRequest, isRSCRequest, isDevWarmupRequest, isHmrRefresh, @@ -1448,41 +1777,7 @@ async function renderToHTMLOrFlightImpl( } } - if (response.collectedTags) { - metadata.fetchTags = response.collectedTags.join(',') - } - - // Let the client router know how long to keep the cached entry around. - const staleHeader = String(response.collectedStale) - res.setHeader(NEXT_ROUTER_STALE_TIME_HEADER, staleHeader) - metadata.headers ??= {} - metadata.headers[NEXT_ROUTER_STALE_TIME_HEADER] = staleHeader - - // If force static is specifically set to false, we should not revalidate - // the page. - if (workStore.forceStatic === false || response.collectedRevalidate === 0) { - metadata.cacheControl = { revalidate: 0, expire: undefined } - } else { - // Copy the cache control value onto the render result metadata. - metadata.cacheControl = { - revalidate: - response.collectedRevalidate >= INFINITE_CACHE - ? false - : response.collectedRevalidate, - expire: - response.collectedExpire >= INFINITE_CACHE - ? undefined - : response.collectedExpire, - } - } - - // provide bailout info for debugging - if (metadata.cacheControl?.revalidate === 0) { - metadata.staticBailoutInfo = { - description: workStore.dynamicUsageDescription, - stack: workStore.dynamicUsageStack, - } - } + applyMetadataFromPrerenderResult(response, metadata, workStore) if (response.renderResumeDataCache) { metadata.renderResumeDataCache = response.renderResumeDataCache @@ -1530,7 +1825,11 @@ async function renderToHTMLOrFlightImpl( if (isDevWarmupRequest) { return warmupDevRender(req, ctx) } else if (isRSCRequest) { - return generateDynamicFlightRenderResult(req, ctx, requestStore) + if (isRuntimePrefetchRequest) { + return generateRuntimePrefetchResult(req, res, ctx, requestStore) + } else { + return generateDynamicFlightRenderResult(req, ctx, requestStore) + } } const renderToStreamWithTracing = getTracer().wrap( @@ -1735,6 +2034,53 @@ export const renderToHTMLOrFlight: AppPageRender = ( ) } +function applyMetadataFromPrerenderResult( + response: Pick< + PrerenderToStreamResult, + | 'collectedExpire' + | 'collectedRevalidate' + | 'collectedStale' + | 'collectedTags' + >, + metadata: AppPageRenderResultMetadata, + workStore: WorkStore +) { + if (response.collectedTags) { + metadata.fetchTags = response.collectedTags.join(',') + } + + // Let the client router know how long to keep the cached entry around. + const staleHeader = String(response.collectedStale) + metadata.headers ??= {} + metadata.headers[NEXT_ROUTER_STALE_TIME_HEADER] = staleHeader + + // If force static is specifically set to false, we should not revalidate + // the page. + if (workStore.forceStatic === false || response.collectedRevalidate === 0) { + metadata.cacheControl = { revalidate: 0, expire: undefined } + } else { + // Copy the cache control value onto the render result metadata. + metadata.cacheControl = { + revalidate: + response.collectedRevalidate >= INFINITE_CACHE + ? false + : response.collectedRevalidate, + expire: + response.collectedExpire >= INFINITE_CACHE + ? undefined + : response.collectedExpire, + } + } + + // provide bailout info for debugging + if (metadata.cacheControl?.revalidate === 0) { + metadata.staticBailoutInfo = { + description: workStore.dynamicUsageDescription, + stack: workStore.dynamicUsageStack, + } + } +} + async function renderToStream( requestStore: RequestStore, req: BaseNextRequest, @@ -2944,11 +3290,7 @@ async function prerenderToStream( setMetadataHeader(name) } - const selectStaleTime = (stale: number) => - stale === INFINITE_CACHE && - typeof experimental.staleTimes?.static === 'number' - ? experimental.staleTimes.static - : stale + const selectStaleTime = createSelectStaleTime(experimental) let prerenderStore: PrerenderStore | null = null @@ -4170,6 +4512,14 @@ const getGlobalErrorStyles = async ( } } +function createSelectStaleTime(experimental: ExperimentalConfig) { + return (stale: number) => + stale === INFINITE_CACHE && + typeof experimental.staleTimes?.static === 'number' + ? experimental.staleTimes.static + : stale +} + async function collectSegmentData( fullPageDataBuffer: Buffer, prerenderStore: PrerenderStore, diff --git a/packages/next/src/server/app-render/collect-segment-data.tsx b/packages/next/src/server/app-render/collect-segment-data.tsx index fea7362a046394..4cf8143a92f1d5 100644 --- a/packages/next/src/server/app-render/collect-segment-data.tsx +++ b/packages/next/src/server/app-render/collect-segment-data.tsx @@ -3,7 +3,6 @@ import type { FlightRouterState, InitialRSCPayload, Segment as FlightRouterStateSegment, - DynamicParamTypesShort, } from './types' import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin' @@ -309,7 +308,7 @@ function collectSegmentDataImpl( } function encodeSegmentWithPossibleFallbackParam( - segment: [string, string, DynamicParamTypesShort], + segment: Exclude, fallbackRouteParams: FallbackRouteParams ): EncodedSegment { const name = segment[0] diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index ee173548df2355..789d95935dabc9 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -300,6 +300,7 @@ async function createComponentTreeInternal( if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': case 'prerender-legacy': case 'prerender-ppr': if (workUnitStore.revalidate > defaultRevalidate) { diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index adc033297c9ca5..5f34aeeed59f79 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -230,6 +230,7 @@ export function trackDynamicDataInDynamicRender(workUnitStore: WorkUnitStore) { // A private cache scope is already dynamic by definition. return case 'prerender': + case 'prerender-runtime': case 'prerender-legacy': case 'prerender-ppr': case 'prerender-client': @@ -335,6 +336,21 @@ export function abortAndThrowOnSynchronousRequestDataAccess( ) } +/** + * Use this function when dynamically prerendering with dynamicIO. + * We don't want to error, because it's better to return something + * (and we've already aborted the render at the point where the sync dynamic error occured), + * but we should log an error server-side. + * @internal + */ +export function warnOnSyncDynamicError(dynamicTracking: DynamicTrackingState) { + if (dynamicTracking.syncDynamicErrorWithStack) { + // the server did something sync dynamic, likely + // leading to an early termination of the prerender. + console.error(dynamicTracking.syncDynamicErrorWithStack) + } +} + // For now these implementations are the same so we just reexport export const trackSynchronousRequestDataAccessInDev = trackSynchronousPlatformIOAccessInDev @@ -519,6 +535,7 @@ export function createHangingInputAbortSignal( ): AbortSignal | undefined { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': const controller = new AbortController() if (workUnitStore.cacheSignal) { @@ -599,6 +616,10 @@ export function useDynamicRouteParams(expression: string) { } break } + case 'prerender-runtime': + throw new InvariantError( + `\`${expression}\` was called during a runtime prerender. Next.js should be preventing ${expression} from being included in server components statically, but did not in this case.` + ) case 'cache': case 'private-cache': throw new InvariantError( diff --git a/packages/next/src/server/app-render/encryption.ts b/packages/next/src/server/app-render/encryption.ts index a7eb39df53565d..142277ad8346e2 100644 --- a/packages/next/src/server/app-render/encryption.ts +++ b/packages/next/src/server/app-render/encryption.ts @@ -260,6 +260,7 @@ export async function decryptActionBoundArgs( switch (workUnitStore?.type) { case 'prerender': + case 'prerender-runtime': // Explicitly don't close the stream here (until prerendering is // complete) so that hanging promises are not rejected. if (workUnitStore.renderSignal.aborted) { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index cf2de626a9fdc7..253ba03eb8bbed 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -36,7 +36,22 @@ export type DynamicParamTypesShort = s.Infer const segmentSchema = s.union([ s.string(), - s.tuple([s.string(), s.string(), dynamicParamTypesSchema]), + s.union([ + s.tuple([s.string(), s.string(), dynamicParamTypesSchema]), + // On the client, the dynamic param array contains an additional + // slot for param value. + s.tuple([ + // Param name + s.string(), + // Param cache key (almost the same as the value, but arrays are + // concatenated into strings) + s.string(), + // Dynamic param type + dynamicParamTypesSchema, + // Param value (the one passed to components) + s.nullable(s.union([s.string(), s.array(s.string())])), + ]), + ]), ]) export type Segment = s.Infer @@ -313,6 +328,9 @@ export type InitialRSCPayload = { b: string /** assetPrefix */ p: string + // TODO: This isn't really the "canonical" URL (which we usually use to refer + // to the URL shown in the browser), it's the URL used to render the page, + // which may have been rewritten on the server. /** initialCanonicalUrlParts */ c: string[] /** couldBeIntercepted */ @@ -333,6 +351,11 @@ export type InitialRSCPayload = { export type NavigationFlightResponse = { /** buildId */ b: string + // TODO: This isn't really the "canonical" URL (which we usually use to refer + // to the URL shown in the browser), it's the URL used to render the page, + // which may have been rewritten on the server. + /** canonicalUrlParts */ + c: string[] /** flightData */ f: FlightData /** prerendered */ @@ -345,6 +368,11 @@ export type ActionFlightResponse = { a: ActionResult /** buildId */ b: string + // TODO: This isn't really the "canonical" URL (which we usually use to refer + // to the URL shown in the browser), it's the URL used to render the page, + // which may have been rewritten on the server. + /** canonicalUrlParts */ + c: string[] /** flightData */ f: FlightData } diff --git a/packages/next/src/server/app-render/use-flight-response.tsx b/packages/next/src/server/app-render/use-flight-response.tsx index efcb291623f183..5457f1c013b0a6 100644 --- a/packages/next/src/server/app-render/use-flight-response.tsx +++ b/packages/next/src/server/app-render/use-flight-response.tsx @@ -71,6 +71,7 @@ export function useFlightStream( flightResponses.set(flightStream, responseOnNextTick) return responseOnNextTick case 'prerender': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': diff --git a/packages/next/src/server/app-render/work-unit-async-storage.external.ts b/packages/next/src/server/app-render/work-unit-async-storage.external.ts index 878f440af2e304..b8eed2737bff17 100644 --- a/packages/next/src/server/app-render/work-unit-async-storage.external.ts +++ b/packages/next/src/server/app-render/work-unit-async-storage.external.ts @@ -82,15 +82,34 @@ export interface RequestStore extends CommonWorkUnitStore { export type PrerenderStoreModern = | PrerenderStoreModernClient | PrerenderStoreModernServer + | PrerenderStoreModernRuntime -export interface PrerenderStoreModernClient extends PrerenderStoreModernCommon { +/** Like `PrerenderStoreModern`, but only including static prerenders (i.e. not runtime prerenders) */ +export type StaticPrerenderStoreModern = Exclude< + PrerenderStoreModern, + PrerenderStoreModernRuntime +> + +export interface PrerenderStoreModernClient + extends PrerenderStoreModernCommon, + StaticPrerenderStoreCommon { readonly type: 'prerender-client' } -export interface PrerenderStoreModernServer extends PrerenderStoreModernCommon { +export interface PrerenderStoreModernServer + extends PrerenderStoreModernCommon, + StaticPrerenderStoreCommon { readonly type: 'prerender' } +export interface PrerenderStoreModernRuntime + extends PrerenderStoreModernCommon { + readonly type: 'prerender-runtime' + + readonly cookies: RequestStore['cookies'] + readonly draftMode: RequestStore['draftMode'] +} + export interface RevalidateStore { // Collected revalidate times and tags for this document during the prerender. revalidate: number // in seconds. 0 means dynamic. INFINITE_CACHE and higher means never revalidate. @@ -139,20 +158,6 @@ interface PrerenderStoreModernCommon readonly rootParams: Params - /** - * The set of unknown route parameters. Accessing these will be tracked as - * a dynamic access. - */ - readonly fallbackRouteParams: FallbackRouteParams | null - - /** - * When true, the page is prerendered as a fallback shell, while allowing any - * dynamic accesses to result in an empty shell. This is the case when there - * are also routes prerendered with a more complete set of params. - * Prerendering those routes would catch any invalid dynamic accesses. - */ - readonly allowEmptyStaticShell: boolean - /** * A mutable resume data cache for this prerender. */ @@ -179,6 +184,22 @@ interface PrerenderStoreModernCommon readonly captureOwnerStack: undefined | (() => string | null) } +interface StaticPrerenderStoreCommon { + /** + * The set of unknown route parameters. Accessing these will be tracked as + * a dynamic access. + */ + readonly fallbackRouteParams: FallbackRouteParams | null + + /** + * When true, the page is prerendered as a fallback shell, while allowing any + * dynamic accesses to result in an empty shell. This is the case when there + * are also routes prerendered with a more complete set of params. + * Prerendering those routes would catch any invalid dynamic accesses. + */ + readonly allowEmptyStaticShell: boolean +} + export interface PrerenderStorePPR extends CommonWorkUnitStore, RevalidateStore { @@ -210,6 +231,12 @@ export type PrerenderStore = | PrerenderStorePPR | PrerenderStoreModern +// /** Like `PrerenderStoreModern`, but only including static prerenders (i.e. not runtime prerenders) */ +export type StaticPrerenderStore = Exclude< + PrerenderStore, + PrerenderStoreModernRuntime +> + export interface CommonCacheStore extends Omit { /** @@ -289,6 +316,7 @@ export function getPrerenderResumeDataCache( ): PrerenderResumeDataCache | null { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': case 'prerender-ppr': return workUnitStore.prerenderResumeDataCache case 'prerender-client': @@ -313,6 +341,7 @@ export function getRenderResumeDataCache( case 'request': return workUnitStore.renderResumeDataCache case 'prerender': + case 'prerender-runtime': case 'prerender-client': if (workUnitStore.renderResumeDataCache) { // If we are in a prerender, we might have a render resume data cache @@ -343,6 +372,7 @@ export function getHmrRefreshHash( case 'cache': case 'private-cache': case 'prerender': + case 'prerender-runtime': return workUnitStore.hmrRefreshHash case 'request': return workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value @@ -359,6 +389,56 @@ export function getHmrRefreshHash( return undefined } +export function isHmrRefresh( + workStore: WorkStore, + workUnitStore: WorkUnitStore +): boolean { + if (workStore.dev) { + switch (workUnitStore.type) { + case 'cache': + case 'private-cache': + case 'request': + return workUnitStore.isHmrRefresh ?? false + case 'prerender': + case 'prerender-client': + case 'prerender-runtime': + case 'prerender-ppr': + case 'prerender-legacy': + case 'unstable-cache': + break + default: + workUnitStore satisfies never + } + } + + return false +} + +export function getServerComponentsHmrCache( + workStore: WorkStore, + workUnitStore: WorkUnitStore +): ServerComponentsHmrCache | undefined { + if (workStore.dev) { + switch (workUnitStore.type) { + case 'cache': + case 'private-cache': + case 'request': + return workUnitStore.serverComponentsHmrCache + case 'prerender': + case 'prerender-client': + case 'prerender-runtime': + case 'prerender-ppr': + case 'prerender-legacy': + case 'unstable-cache': + break + default: + workUnitStore satisfies never + } + } + + return undefined +} + /** * Returns a draft mode provider only if draft mode is enabled. */ @@ -371,6 +451,7 @@ export function getDraftModeProviderForCacheScope( case 'cache': case 'private-cache': case 'unstable-cache': + case 'prerender-runtime': case 'request': return workUnitStore.draftMode case 'prerender': @@ -392,6 +473,7 @@ export function getCacheSignal( switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': return workUnitStore.cacheSignal case 'prerender-ppr': case 'prerender-legacy': diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index c4cc9c7f6eba5a..24f379ea36d094 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2015,16 +2015,26 @@ export default abstract class Server< ) { const headers = req.headers - const isPrefetchRSCRequest = - headers[NEXT_ROUTER_PREFETCH_HEADER] || - getRequestMeta(req, 'isPrefetchRSCRequest') + const prefetchHeaderValue = headers[NEXT_ROUTER_PREFETCH_HEADER] + const routerPrefetch = + prefetchHeaderValue !== undefined + ? // We only recognize '1' and '2'. Strip all other values here. + prefetchHeaderValue === '1' || prefetchHeaderValue === '2' + ? prefetchHeaderValue + : undefined + : // For runtime prefetches, we always perform a dynamic request, + // so we don't expect the header to be stripped by an intermediate layer. + // This should only happen for static prefetches, so we only handle those here. + getRequestMeta(req, 'isPrefetchRSCRequest') + ? '1' + : undefined const segmentPrefetchRSCRequest = headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] || getRequestMeta(req, 'segmentPrefetchRSCRequest') const expectedHash = computeCacheBustingSearchParam( - isPrefetchRSCRequest ? '1' : '0', + routerPrefetch, segmentPrefetchRSCRequest, headers[NEXT_ROUTER_STATE_TREE_HEADER], headers[NEXT_URL] diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 9324228b0d73b5..268ce6c875e381 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -370,6 +370,7 @@ export function createPatchedFetcher( if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': // TODO: Stop accumulating tags in client prerender. (fallthrough) case 'prerender-client': case 'prerender-ppr': @@ -412,6 +413,7 @@ export function createPatchedFetcher( break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': @@ -552,6 +554,7 @@ export function createPatchedFetcher( if (hasNoExplicitCacheConfig && workUnitStore !== undefined) { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': // While we don't want to do caching in the client scope we know the // fetch will be dynamic for cacheComponents so we may as well avoid the // call here. (fallthrough) @@ -668,6 +671,7 @@ export function createPatchedFetcher( switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': if (cacheSignal) { cacheSignal.endRead() cacheSignal = null @@ -721,6 +725,7 @@ export function createPatchedFetcher( break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'unstable-cache': @@ -840,6 +845,7 @@ export function createPatchedFetcher( switch (workUnitStore?.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': return createCachedPrerenderResponse( res, cacheKey, @@ -912,6 +918,7 @@ export function createPatchedFetcher( switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': // We sometimes use the cache to dedupe fetches that do not // specify a cache configuration. In these cases we want to // make sure we still exclude them from prerenders if @@ -1013,6 +1020,7 @@ export function createPatchedFetcher( switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': if (cacheSignal) { cacheSignal.endRead() cacheSignal = null @@ -1052,6 +1060,7 @@ export function createPatchedFetcher( switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': return makeHangingPromise( workUnitStore.renderSignal, 'fetch()' diff --git a/packages/next/src/server/node-environment-extensions/console-dev.tsx b/packages/next/src/server/node-environment-extensions/console-dev.tsx index 31a0e165c4cbb9..f783bf7a0690b4 100644 --- a/packages/next/src/server/node-environment-extensions/console-dev.tsx +++ b/packages/next/src/server/node-environment-extensions/console-dev.tsx @@ -141,6 +141,7 @@ function patchConsoleMethodDEV(methodName: InterceptableConsoleMethod): void { switch (workUnitStore?.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': originalMethod.apply(this, dimConsoleCall(methodName, args)) break case 'prerender-ppr': diff --git a/packages/next/src/server/node-environment-extensions/utils.tsx b/packages/next/src/server/node-environment-extensions/utils.tsx index f45ada37b6767b..9e147e1a344e36 100644 --- a/packages/next/src/server/node-environment-extensions/utils.tsx +++ b/packages/next/src/server/node-environment-extensions/utils.tsx @@ -20,7 +20,8 @@ export function io(expression: string, type: ApiType) { } switch (workUnitStore.type) { - case 'prerender': { + case 'prerender': + case 'prerender-runtime': { const prerenderSignal = workUnitStore.controller.signal if (prerenderSignal.aborted === false) { diff --git a/packages/next/src/server/request/connection.ts b/packages/next/src/server/request/connection.ts index 37f8be43586079..679a3d132ac57e 100644 --- a/packages/next/src/server/request/connection.ts +++ b/packages/next/src/server/request/connection.ts @@ -53,7 +53,7 @@ export function connection(): Promise { } case 'private-cache': { // It might not be intuitive to throw for private caches as well, but - // we don't consider dynamic prefetches as "actual requests" (in the + // we don't consider runtime prefetches as "actual requests" (in the // navigation sense), despite allowing them to read cookies. const error = new Error( `Route ${workStore.route} used "connection" inside "use cache: private". The \`connection()\` function is used to indicate the subsequent code must only run when there is an actual navigation request, but caches must be able to be produced before a navigation request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache` @@ -68,6 +68,7 @@ export function connection(): Promise { ) case 'prerender': case 'prerender-client': + case 'prerender-runtime': // We return a promise that never resolves to allow the prerender to // stall at this point. return makeHangingPromise( diff --git a/packages/next/src/server/request/cookies.ts b/packages/next/src/server/request/cookies.ts index 826c143f672c93..73ddae8458c19a 100644 --- a/packages/next/src/server/request/cookies.ts +++ b/packages/next/src/server/request/cookies.ts @@ -114,6 +114,7 @@ export function cookies(): Promise { workStore, workUnitStore ) + case 'prerender-runtime': case 'private-cache': return makeUntrackedExoticCookies(workUnitStore.cookies) case 'request': @@ -470,6 +471,7 @@ function syncIODev(route: string | undefined, expression: string) { break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'cache': diff --git a/packages/next/src/server/request/draft-mode.ts b/packages/next/src/server/request/draft-mode.ts index 52026725916106..a58f364af19bbc 100644 --- a/packages/next/src/server/request/draft-mode.ts +++ b/packages/next/src/server/request/draft-mode.ts @@ -55,6 +55,7 @@ export function draftMode(): Promise { } switch (workUnitStore.type) { + case 'prerender-runtime': case 'request': return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore) @@ -62,8 +63,8 @@ export function draftMode(): Promise { case 'private-cache': case 'unstable-cache': // Inside of `"use cache"` or `unstable_cache`, draft mode is available if - // the outmost work unit store is a request store, and if draft mode is - // enabled. + // the outmost work unit store is a request store (or a runtime prerender), + // and if draft mode is enabled. const draftModeProvider = getDraftModeProviderForCacheScope( workStore, workUnitStore @@ -257,6 +258,7 @@ function syncIODev(route: string | undefined, expression: string) { break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'cache': @@ -322,7 +324,9 @@ function trackDynamicDraftMode(expression: string, constructorOpt: Function) { throw new Error( `Route ${workStore.route} used "${expression}" inside a function cached with "unstable_cache(...)". The enabled status of draftMode can be read in caches but you must not enable or disable draftMode inside a cache. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) - case 'prerender': { + + case 'prerender': + case 'prerender-runtime': { const error = new Error( `Route ${workStore.route} used ${expression} without first calling \`await connection()\`. See more info here: https://nextjs.org/docs/messages/next-prerender-sync-headers` ) diff --git a/packages/next/src/server/request/headers.ts b/packages/next/src/server/request/headers.ts index c7dafb6d21acc7..28189e018b9708 100644 --- a/packages/next/src/server/request/headers.ts +++ b/packages/next/src/server/request/headers.ts @@ -101,6 +101,7 @@ export function headers(): Promise { ) case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': @@ -119,6 +120,7 @@ export function headers(): Promise { if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': + case 'prerender-runtime': return makeHangingHeaders(workUnitStore) case 'prerender-client': const exportName = '`headers`' @@ -433,6 +435,7 @@ function syncIODev(route: string | undefined, expression: string) { break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'cache': diff --git a/packages/next/src/server/request/params.ts b/packages/next/src/server/request/params.ts index c4e97d072d4da0..143a6e30c5090c 100644 --- a/packages/next/src/server/request/params.ts +++ b/packages/next/src/server/request/params.ts @@ -10,10 +10,10 @@ import { import { workUnitAsyncStorage, - type PrerenderStore, type PrerenderStorePPR, type PrerenderStoreLegacy, - type PrerenderStoreModern, + type StaticPrerenderStoreModern, + type StaticPrerenderStore, } from '../app-render/work-unit-async-storage.external' import { InvariantError } from '../../shared/lib/invariant-error' import { @@ -74,6 +74,10 @@ export function createParamsFromClient( throw new InvariantError( 'createParamsFromClient should not be called in cache contexts.' ) + case 'prerender-runtime': + throw new InvariantError( + 'createParamsFromClient should not be called in a runtime prerender.' + ) case 'request': break default: @@ -106,6 +110,7 @@ export function createServerParamsForRoute( throw new InvariantError( 'createServerParamsForRoute should not be called in cache contexts.' ) + case 'prerender-runtime': case 'request': break default: @@ -133,6 +138,7 @@ export function createServerParamsForServerSegment( throw new InvariantError( 'createServerParamsForServerSegment should not be called in cache contexts.' ) + case 'prerender-runtime': case 'request': break default: @@ -171,6 +177,7 @@ export function createPrerenderParamsForClientSegment( ) case 'prerender-ppr': case 'prerender-legacy': + case 'prerender-runtime': case 'request': break default: @@ -186,7 +193,7 @@ export function createPrerenderParamsForClientSegment( function createPrerenderParams( underlyingParams: Params, workStore: WorkStore, - prerenderStore: PrerenderStore + prerenderStore: StaticPrerenderStore ): Promise { switch (prerenderStore.type) { case 'prerender': @@ -291,7 +298,7 @@ const fallbackParamsProxyHandler: ProxyHandler> = { function makeHangingParams( underlyingParams: Params, - prerenderStore: PrerenderStoreModern + prerenderStore: StaticPrerenderStoreModern ): Promise { const cachedParams = CachedParams.get(underlyingParams) if (cachedParams) { @@ -578,6 +585,7 @@ function syncIODev( break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'cache': diff --git a/packages/next/src/server/request/pathname.ts b/packages/next/src/server/request/pathname.ts index 15ea7656e32e6e..b13f82dd73ef87 100644 --- a/packages/next/src/server/request/pathname.ts +++ b/packages/next/src/server/request/pathname.ts @@ -7,7 +7,7 @@ import { import { workUnitAsyncStorage, - type PrerenderStore, + type StaticPrerenderStore, } from '../app-render/work-unit-async-storage.external' import { makeHangingPromise } from '../dynamic-rendering-utils' import { InvariantError } from '../../shared/lib/invariant-error' @@ -35,6 +35,8 @@ export function createServerPathnameForMetadata( throw new InvariantError( 'createServerPathnameForMetadata should not be called in cache contexts.' ) + + case 'prerender-runtime': case 'request': break default: @@ -47,7 +49,7 @@ export function createServerPathnameForMetadata( function createPrerenderPathname( underlyingPathname: string, workStore: WorkStore, - prerenderStore: PrerenderStore + prerenderStore: StaticPrerenderStore ): Promise { switch (prerenderStore.type) { case 'prerender-client': diff --git a/packages/next/src/server/request/root-params.ts b/packages/next/src/server/request/root-params.ts index e3fb74a4f72ab8..3ee448a9dfea28 100644 --- a/packages/next/src/server/request/root-params.ts +++ b/packages/next/src/server/request/root-params.ts @@ -9,9 +9,9 @@ import { } from '../app-render/work-async-storage.external' import { workUnitAsyncStorage, - type PrerenderStore, type PrerenderStoreLegacy, type PrerenderStorePPR, + type StaticPrerenderStore, } from '../app-render/work-unit-async-storage.external' import { makeHangingPromise } from '../dynamic-rendering-utils' import type { FallbackRouteParams } from './fallback-params' @@ -56,6 +56,7 @@ export async function unstable_rootParams(): Promise { workUnitStore ) case 'private-cache': + case 'prerender-runtime': case 'request': return Promise.resolve(workUnitStore.rootParams) default: @@ -66,7 +67,7 @@ export async function unstable_rootParams(): Promise { function createPrerenderRootParams( underlyingParams: Params, workStore: WorkStore, - prerenderStore: PrerenderStore + prerenderStore: StaticPrerenderStore ): Promise { switch (prerenderStore.type) { case 'prerender-client': { @@ -245,6 +246,7 @@ export function getRootParam(paramName: string): Promise { ) } case 'private-cache': + case 'prerender-runtime': case 'request': { break } @@ -258,7 +260,7 @@ export function getRootParam(paramName: string): Promise { function createPrerenderRootParamPromise( paramName: string, workStore: WorkStore, - prerenderStore: PrerenderStore, + prerenderStore: StaticPrerenderStore, apiName: string ): Promise { switch (prerenderStore.type) { diff --git a/packages/next/src/server/request/search-params.ts b/packages/next/src/server/request/search-params.ts index 33eec43be6fb54..c0aef92fcbb606 100644 --- a/packages/next/src/server/request/search-params.ts +++ b/packages/next/src/server/request/search-params.ts @@ -11,10 +11,10 @@ import { import { workUnitAsyncStorage, - type PrerenderStore, type PrerenderStoreLegacy, type PrerenderStorePPR, type PrerenderStoreModern, + type StaticPrerenderStore, } from '../app-render/work-unit-async-storage.external' import { InvariantError } from '../../shared/lib/invariant-error' import { makeHangingPromise } from '../dynamic-rendering-utils' @@ -72,6 +72,10 @@ export function createSearchParamsFromClient( case 'prerender-ppr': case 'prerender-legacy': return createPrerenderSearchParams(workStore, workUnitStore) + case 'prerender-runtime': + throw new InvariantError( + 'createSearchParamsFromClient should not be called in a runtime prerender.' + ) case 'cache': case 'private-cache': case 'unstable-cache': @@ -109,6 +113,7 @@ export function createServerSearchParamsForServerPage( throw new InvariantError( 'createServerSearchParamsForServerPage should not be called in cache contexts.' ) + case 'prerender-runtime': case 'request': break default: @@ -135,6 +140,10 @@ export function createPrerenderSearchParamsForClientPage( // We're prerendering in a mode that aborts (cacheComponents) and should stall // the promise to ensure the RSC side is considered dynamic return makeHangingPromise(workUnitStore.renderSignal, '`searchParams`') + case 'prerender-runtime': + throw new InvariantError( + 'createPrerenderSearchParamsForClientPage should not be called in a runtime prerender.' + ) case 'cache': case 'private-cache': case 'unstable-cache': @@ -157,7 +166,7 @@ export function createPrerenderSearchParamsForClientPage( function createPrerenderSearchParams( workStore: WorkStore, - prerenderStore: PrerenderStore + prerenderStore: StaticPrerenderStore ): Promise { if (workStore.forceStatic) { // When using forceStatic we override all other logic and always just return an empty @@ -803,6 +812,7 @@ function syncIODev( break case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'cache': diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 1183b8d444fc0b..3cabc7afba0e54 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -1189,6 +1189,10 @@ function trackDynamic( throw new InvariantError( 'A client prerender store should not be used for a route handler.' ) + case 'prerender-runtime': + throw new InvariantError( + 'A runtime prerender store should not be used for a route handler.' + ) case 'prerender-ppr': return postponeWithTracking( store.route, diff --git a/packages/next/src/server/use-cache/cache-life.ts b/packages/next/src/server/use-cache/cache-life.ts index d07fd792e3f6dd..a02b9234b0ff6b 100644 --- a/packages/next/src/server/use-cache/cache-life.ts +++ b/packages/next/src/server/use-cache/cache-life.ts @@ -96,6 +96,7 @@ export function cacheLife(profile: CacheLifeProfiles | CacheLife): void { switch (workUnitStore?.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': diff --git a/packages/next/src/server/use-cache/cache-tag.ts b/packages/next/src/server/use-cache/cache-tag.ts index 7932a2dc4eb13d..321c9424964554 100644 --- a/packages/next/src/server/use-cache/cache-tag.ts +++ b/packages/next/src/server/use-cache/cache-tag.ts @@ -13,6 +13,7 @@ export function cacheTag(...tags: string[]): void { switch (workUnitStore?.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': diff --git a/packages/next/src/server/use-cache/constants.ts b/packages/next/src/server/use-cache/constants.ts index a5be3cd9cdbf9e..ca7e76d2c26d1d 100644 --- a/packages/next/src/server/use-cache/constants.ts +++ b/packages/next/src/server/use-cache/constants.ts @@ -1,2 +1,2 @@ export const DYNAMIC_EXPIRE = 300 // 5 minutes -export const DYNAMIC_PREFETCH_DYNAMIC_STALE = 30 // 30 seconds +export const RUNTIME_PREFETCH_DYNAMIC_STALE = 30 // 30 seconds 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 8c28b6de2f4e11..dd94d314df8396 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -18,6 +18,7 @@ import type { WorkStore } from '../app-render/work-async-storage.external' import { workAsyncStorage } from '../app-render/work-async-storage.external' import type { PrerenderStoreModernClient, + PrerenderStoreModernRuntime, PrivateUseCacheStore, RequestStore, RevalidateStore, @@ -31,6 +32,8 @@ import { workUnitAsyncStorage, getDraftModeProviderForCacheScope, getCacheSignal, + isHmrRefresh, + getServerComponentsHmrCache, } from '../app-render/work-unit-async-storage.external' import { makeHangingPromise } from '../dynamic-rendering-utils' @@ -46,7 +49,7 @@ import type { CacheSignal } from '../app-render/cache-signal' import { decryptActionBoundArgs } from '../app-render/encryption' import { InvariantError } from '../../shared/lib/invariant-error' import { getDigestForWellKnownError } from '../app-render/create-error-handler' -import { DYNAMIC_EXPIRE, DYNAMIC_PREFETCH_DYNAMIC_STALE } from './constants' +import { DYNAMIC_EXPIRE, RUNTIME_PREFETCH_DYNAMIC_STALE } from './constants' import { getCacheHandler } from './handlers' import { UseCacheTimeoutError } from './use-cache-errors' import { @@ -67,8 +70,10 @@ import type { CacheLife } from './cache-life' interface PrivateCacheContext { readonly kind: 'private' - // TODO: Add dynamic prefetching store when this exists. - readonly outerWorkUnitStore: RequestStore | PrivateUseCacheStore + readonly outerWorkUnitStore: + | RequestStore + | PrivateUseCacheStore + | PrerenderStoreModernRuntime } interface PublicCacheContext { @@ -188,8 +193,11 @@ function createUseCacheStore( explicitStale: undefined, tags: null, hmrRefreshHash: getHmrRefreshHash(workStore, outerWorkUnitStore), - isHmrRefresh: outerWorkUnitStore.isHmrRefresh ?? false, - serverComponentsHmrCache: outerWorkUnitStore.serverComponentsHmrCache, + isHmrRefresh: isHmrRefresh(workStore, outerWorkUnitStore), + serverComponentsHmrCache: getServerComponentsHmrCache( + workStore, + outerWorkUnitStore + ), forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore), draftMode: getDraftModeProviderForCacheScope( workStore, @@ -209,6 +217,7 @@ function createUseCacheStore( case 'request': useCacheOrRequestStore = outerWorkUnitStore break + case 'prerender-runtime': case 'prerender': case 'prerender-ppr': case 'prerender-legacy': @@ -324,8 +333,8 @@ function propagateCacheLifeAndTags( entry: CacheEntry ): void { if (cacheContext.kind === 'private') { - switch (cacheContext.outerWorkUnitStore?.type) { - // TODO: Also propagate cache life and tags to dynamic prefetching stores. + switch (cacheContext.outerWorkUnitStore.type) { + case 'prerender-runtime': case 'private-cache': propagateCacheLifeAndTagsToRevalidateStore( cacheContext.outerWorkUnitStore, @@ -343,6 +352,7 @@ function propagateCacheLifeAndTags( case 'cache': case 'private-cache': case 'prerender': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': propagateCacheLifeAndTagsToRevalidateStore( @@ -489,6 +499,7 @@ async function generateCacheEntryImpl( if (outerWorkUnitStore) { switch (outerWorkUnitStore.type) { + case 'prerender-runtime': case 'prerender': // The encoded arguments might contain hanging promises. In // this case we don't want to reject with "Error: Connection @@ -565,7 +576,7 @@ async function generateCacheEntryImpl( let stream: ReadableStream switch (outerWorkUnitStore?.type) { - // TODO: Dynamic prefetches should also use the prerender variant. + case 'prerender-runtime': case 'prerender': const timeoutAbortController = new AbortController() @@ -862,6 +873,7 @@ export function cache( ) } case 'request': + case 'prerender-runtime': case 'private-cache': cacheContext = { kind: 'private', @@ -891,6 +903,7 @@ export function cache( `${expression} must not be used within a client component. Next.js should be preventing ${expression} from being allowed in client components statically, but did not in this case.` ) case 'prerender': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': @@ -1032,8 +1045,8 @@ export function cache( // need to include the cookies in the cache key. This is because we don't // store the cache entries in a cache handler, but only in the Resume Data // Cache (RDC). Private caches are only used during dynamic requests and - // dynamic prefetches. For dynamic requests, the RDC is immutable, so it - // does not include any private caches. For dynamic prefetches, the RDC is + // runtime prefetches. For dynamic requests, the RDC is immutable, so it + // does not include any private caches. For runtime prefetches, the RDC is // mutable, but only lives as long as the request, so the key does not // need to include cookies. const cacheKeyParts: CacheKeyParts = hmrRefreshHash @@ -1049,6 +1062,7 @@ export function cache( let encodedCacheKeyParts: FormData | string switch (workUnitStore?.type) { + case 'prerender-runtime': case 'prerender': if (!isPageOrLayout) { // If the "use cache" function is not a page or a layout, we need to @@ -1135,6 +1149,7 @@ export function cache( workUnitStore.renderSignal, 'dynamic "use cache"' ) + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': @@ -1147,8 +1162,31 @@ export function cache( } } - if (existingEntry.stale < DYNAMIC_PREFETCH_DYNAMIC_STALE) { - // TODO: Return hanging promise for dynamic prefetches. + if (existingEntry.stale < RUNTIME_PREFETCH_DYNAMIC_STALE) { + switch (workUnitStore.type) { + case 'prerender-runtime': + // In a runtime prerender, if the cache entry will become stale in less then 30 seconds, + // we consider this cache entry dynamic as it's not worth prefetching. + // It's better to leave a PPR hole that can be filled in dynamically + // with a potentially cached entry. + if (cacheSignal) { + cacheSignal.endRead() + } + return makeHangingPromise( + workUnitStore.renderSignal, + 'dynamic "use cache"' + ) + case 'prerender': + case 'prerender-ppr': + case 'prerender-legacy': + case 'request': + case 'cache': + case 'private-cache': + case 'unstable-cache': + break + default: + workUnitStore satisfies never + } } } @@ -1192,6 +1230,7 @@ export function cache( ) } break + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': @@ -1288,6 +1327,7 @@ export function cache( workUnitStore.renderSignal, 'dynamic "use cache"' ) + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'request': @@ -1536,6 +1576,7 @@ function shouldForceRevalidate( case 'cache': case 'private-cache': return workUnitStore.forceRevalidate + case 'prerender-runtime': case 'prerender': case 'prerender-client': case 'prerender-ppr': @@ -1579,6 +1620,7 @@ function shouldDiscardCacheEntry( switch (workUnitStore.type) { case 'prerender': return false + case 'prerender-runtime': case 'prerender-client': case 'prerender-ppr': case 'prerender-legacy': diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index a2c9e014466f87..09282979a5f91e 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -298,9 +298,8 @@ export async function adapter( onAfterTaskError: undefined, }, requestEndedState: { ended: false }, - isPrefetchRequest: request.headers.has( - NEXT_ROUTER_PREFETCH_HEADER - ), + isPrefetchRequest: + request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1', buildId: buildId ?? '', previouslyRevalidatedTags: [], }) diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index 0b00fef360a87e..4cd7c62bc396b2 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -110,6 +110,7 @@ function revalidate(tags: string[], expression: string) { `Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering` ) case 'prerender': + case 'prerender-runtime': // cacheComponents Prerender const error = new Error( `Route ${store.route} used ${expression} without first calling \`await connection()\`.` diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index f579a0dd8582e1..555e88541e5b6a 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -163,6 +163,7 @@ export function unstable_cache( case 'cache': case 'private-cache': case 'prerender': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': // We update the store's revalidate property if the option.revalidate is a higher precedence @@ -384,6 +385,7 @@ function getFetchUrlPrefix( return `${pathname}${sortedSearch.length ? '?' : ''}${sortedSearch}` case 'prerender': case 'prerender-client': + case 'prerender-runtime': case 'prerender-ppr': case 'prerender-legacy': case 'cache': diff --git a/packages/next/src/server/web/spec-extension/unstable-no-store.ts b/packages/next/src/server/web/spec-extension/unstable-no-store.ts index 1f87cfa05cbd3b..4d6926b23e52f7 100644 --- a/packages/next/src/server/web/spec-extension/unstable-no-store.ts +++ b/packages/next/src/server/web/spec-extension/unstable-no-store.ts @@ -34,6 +34,7 @@ export function unstable_noStore() { switch (workUnitStore.type) { case 'prerender': case 'prerender-client': + case 'prerender-runtime': // unstable_noStore() is a noop in Dynamic I/O. return case 'prerender-ppr': diff --git a/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts b/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts index c08f7644e9b701..ac671ec1e26480 100644 --- a/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts +++ b/packages/next/src/shared/lib/router/utils/cache-busting-search-param.ts @@ -1,7 +1,7 @@ import { hexHash } from '../../hash' export function computeCacheBustingSearchParam( - prefetchHeader: '1' | '0' | undefined, + prefetchHeader: '1' | '2' | '0' | undefined, segmentPrefetchHeader: string | string[] | undefined, stateTreeHeader: string | string[] | undefined, nextUrlHeader: string | string[] | undefined diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 8adda4da79f1d8..99a6f8a67b4e19 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.4.2-canary.24", + "version": "15.4.2-canary.25", "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 9423390ea080d6..2cc72d54db9f91 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.4.2-canary.24", + "version": "15.4.2-canary.25", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.4.2-canary.24", + "next": "15.4.2-canary.25", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2b11c8f882e41..9203a312a6e193 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,10 +17,10 @@ overrides: '@types/react-dom': 19.1.6 '@types/retry': 0.12.0 jest-snapshot: 30.0.0-alpha.6 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 - react-is: 19.2.0-canary-9784cb37-20250730 - scheduler: 0.27.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 + react-is: 19.2.0-canary-c260b38d-20250731 + scheduler: 0.27.0-canary-c260b38d-20250731 patchedDependencies: '@ampproject/toolbox-optimizer@2.8.3': @@ -87,7 +87,7 @@ importers: version: 11.11.0 '@emotion/react': specifier: 11.11.1 - version: 11.11.1(@types/react@19.1.8)(react@19.2.0-canary-9784cb37-20250730) + version: 11.11.1(@types/react@19.1.8)(react@19.2.0-canary-c260b38d-20250731) '@fullhuman/postcss-purgecss': specifier: 1.3.0 version: 1.3.0 @@ -99,7 +99,7 @@ importers: version: 2.2.1(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))) '@mdx-js/react': specifier: 2.2.1 - version: 2.2.1(react@19.2.0-canary-9784cb37-20250730) + version: 2.2.1(react@19.2.0-canary-c260b38d-20250731) '@next/bundle-analyzer': specifier: workspace:* version: link:packages/next-bundle-analyzer @@ -165,7 +165,7 @@ importers: version: 6.1.2(@jest/globals@29.7.0)(@types/jest@29.5.5)(jest@29.7.0(@types/node@20.17.6(patch_hash=rvl3vkomen3tospgr67bzubfyu))(babel-plugin-macros@3.1.0))(vitest@3.0.4(@types/node@20.17.6(patch_hash=rvl3vkomen3tospgr67bzubfyu))(sass@1.54.0)(tsx@4.19.2)) '@testing-library/react': specifier: ^15.0.5 - version: 15.0.7(@types/react@19.1.8)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + version: 15.0.7(@types/react@19.1.8)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) '@types/busboy': specifier: 1.5.3 version: 1.5.3 @@ -485,44 +485,44 @@ importers: specifier: 0.3.0 version: 0.3.0 react: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730 + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731 react-builtin: - specifier: npm:react@19.2.0-canary-9784cb37-20250730 - version: react@19.2.0-canary-9784cb37-20250730 + specifier: npm:react@19.2.0-canary-c260b38d-20250731 + version: react@19.2.0-canary-c260b38d-20250731 react-dom: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) react-dom-builtin: - specifier: npm:react-dom@19.2.0-canary-9784cb37-20250730 - version: react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + specifier: npm:react-dom@19.2.0-canary-c260b38d-20250731 + version: react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) react-dom-experimental-builtin: - specifier: npm:react-dom@0.0.0-experimental-9784cb37-20250730 - version: react-dom@0.0.0-experimental-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + specifier: npm:react-dom@0.0.0-experimental-c260b38d-20250731 + version: react-dom@0.0.0-experimental-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) react-experimental-builtin: - specifier: npm:react@0.0.0-experimental-9784cb37-20250730 - version: react@0.0.0-experimental-9784cb37-20250730 + specifier: npm:react@0.0.0-experimental-c260b38d-20250731 + version: react@0.0.0-experimental-c260b38d-20250731 react-is-builtin: - specifier: npm:react-is@19.2.0-canary-9784cb37-20250730 - version: react-is@19.2.0-canary-9784cb37-20250730 + specifier: npm:react-is@19.2.0-canary-c260b38d-20250731 + version: react-is@19.2.0-canary-c260b38d-20250731 react-server-dom-turbopack: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) react-server-dom-turbopack-experimental: - specifier: npm:react-server-dom-turbopack@0.0.0-experimental-9784cb37-20250730 - version: react-server-dom-turbopack@0.0.0-experimental-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + specifier: npm:react-server-dom-turbopack@0.0.0-experimental-c260b38d-20250731 + version: react-server-dom-turbopack@0.0.0-experimental-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) react-server-dom-webpack: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))) + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))) react-server-dom-webpack-experimental: - specifier: npm:react-server-dom-webpack@0.0.0-experimental-9784cb37-20250730 - version: react-server-dom-webpack@0.0.0-experimental-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))) + specifier: npm:react-server-dom-webpack@0.0.0-experimental-c260b38d-20250731 + version: react-server-dom-webpack@0.0.0-experimental-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))) react-ssr-prepass: specifier: 1.0.8 - version: 1.0.8(react-is@19.2.0-canary-eaee5308-20250728)(react@19.2.0-canary-9784cb37-20250730) + version: 1.0.8(react-is@19.2.0-canary-eaee5308-20250728)(react@19.2.0-canary-c260b38d-20250731) react-virtualized: specifier: 9.22.3 - version: 9.22.3(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + version: 9.22.3(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) relay-compiler: specifier: 13.0.2 version: 13.0.2 @@ -545,11 +545,11 @@ importers: specifier: 0.15.2 version: 0.15.2 scheduler-builtin: - specifier: npm:scheduler@0.27.0-canary-9784cb37-20250730 - version: scheduler@0.27.0-canary-9784cb37-20250730 + specifier: npm:scheduler@0.27.0-canary-c260b38d-20250731 + version: scheduler@0.27.0-canary-c260b38d-20250731 scheduler-experimental-builtin: - specifier: npm:scheduler@0.0.0-experimental-9784cb37-20250730 - version: scheduler@0.0.0-experimental-9784cb37-20250730 + specifier: npm:scheduler@0.0.0-experimental-c260b38d-20250731 + version: scheduler@0.0.0-experimental-c260b38d-20250731 seedrandom: specifier: 3.0.5 version: 3.0.5 @@ -567,13 +567,13 @@ importers: version: 6.0.0 styled-jsx: specifier: 5.1.6 - version: 5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-9784cb37-20250730) + version: 5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-c260b38d-20250731) styled-jsx-plugin-postcss: specifier: 3.0.2 version: 3.0.2 swr: specifier: ^2.2.4 - version: 2.2.4(react@19.2.0-canary-9784cb37-20250730) + version: 2.2.4(react@19.2.0-canary-c260b38d-20250731) tailwindcss: specifier: 3.2.7 version: 3.2.7(postcss@8.4.31) @@ -851,7 +851,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -921,7 +921,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -933,17 +933,17 @@ importers: specifier: 8.4.31 version: 8.4.31 react: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730 + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731 react-dom: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) sass: specifier: ^1.3.0 version: 1.77.8 styled-jsx: specifier: 5.1.6 - version: 5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-9784cb37-20250730) + version: 5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-c260b38d-20250731) optionalDependencies: sharp: specifier: ^0.34.3 @@ -1017,7 +1017,7 @@ importers: version: 7.27.0 '@base-ui-components/react': specifier: 1.0.0-beta.2 - version: 1.0.0-beta.2(@types/react@19.1.8)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + version: 1.0.0-beta.2(@types/react@19.1.8)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) '@capsizecss/metrics': specifier: 3.4.0 version: 3.4.0 @@ -1046,19 +1046,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../font '@next/polyfill-module': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../react-refresh-utils '@next/swc': - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1083,13 +1083,13 @@ importers: version: 3.0.0(@swc/helpers@0.5.15)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@storybook/blocks': specifier: 8.6.0 - version: 8.6.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3)) + version: 8.6.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3)) '@storybook/react': specifier: 8.6.0 - version: 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + version: 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/react-webpack5': specifier: 8.6.0 - version: 8.6.0(@rspack/core@1.4.5(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + version: 8.6.0(@rspack/core@1.4.5(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/test': specifier: 8.6.0 version: 8.6.0(storybook@8.6.0(prettier@3.3.3)) @@ -1581,7 +1581,7 @@ importers: version: 1.0.35 unistore: specifier: 3.4.1 - version: 3.4.1(react@19.2.0-canary-9784cb37-20250730) + version: 3.4.1(react@19.2.0-canary-c260b38d-20250731) util: specifier: 0.12.4 version: 0.12.4 @@ -1754,14 +1754,14 @@ importers: packages/third-parties: dependencies: react: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730 + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731 third-party-capital: specifier: 1.0.20 version: 1.0.20 devDependencies: next: - specifier: 15.4.2-canary.24 + specifier: 15.4.2-canary.25 version: link:../next outdent: specifier: 0.8.0 @@ -1818,14 +1818,14 @@ importers: specifier: 29.5.0 version: 29.5.0 react: - specifier: 19.2.0-canary-9784cb37-20250730 - version: 19.2.0-canary-9784cb37-20250730 + specifier: 19.2.0-canary-c260b38d-20250731 + version: 19.2.0-canary-c260b38d-20250731 react-test-renderer: specifier: 18.2.0 - version: 18.2.0(react@19.2.0-canary-9784cb37-20250730) + version: 18.2.0(react@19.2.0-canary-c260b38d-20250731) styled-jsx: specifier: ^5.1.2 - version: 5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-9784cb37-20250730) + version: 5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-c260b38d-20250731) turbopack/packages/devlow-bench: dependencies: @@ -2753,8 +2753,8 @@ packages: engines: {node: '>=14.0.0'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -2763,8 +2763,8 @@ packages: resolution: {integrity: sha512-9+uaWyF1o/PgXqHLJnC81IIG0HlV3o9eFCQ5hWZDMx5NHrFk0rrwqEFGQOB8lti/rnbxNPi+kYYw1D4e8xSn/Q==} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -2979,7 +2979,7 @@ packages: resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} peerDependencies: '@types/react': '*' - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -2996,7 +2996,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks@1.0.1': resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@emotion/utils@1.2.1': resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} @@ -3663,20 +3663,20 @@ packages: '@floating-ui/react-dom@2.1.0': resolution: {integrity: sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 '@floating-ui/react-dom@2.1.5': resolution: {integrity: sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 '@floating-ui/react@0.26.16': resolution: {integrity: sha512-HEf43zxZNAI/E781QIVpYSF3K2VH4TTYZpqecjdsFkjsaU1EbaWcM++kw0HXFffj7gDUcBFevX8s0rQGQpxkow==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} @@ -4400,13 +4400,13 @@ packages: resolution: {integrity: sha512-l9ypojKN3PjwO1CSLIsqxi7mA25+7w+xc71Q+JuCCREI0tuGwkZsKbIOpuTATIJOjPh8ycLiW7QxX1LYsRTq6w==} peerDependencies: '@mantine/hooks': 7.10.1 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 '@mantine/hooks@7.11.2': resolution: {integrity: sha512-jhyVe/sbDEG2U8rr2lMecUPgQxcfr5hh9HazqGfkS7ZRIMDO7uJ947yAcTMGGkp5Lxtt5TBFt1Cb6tiB2/1agg==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -4429,13 +4429,13 @@ packages: '@mdx-js/react@2.2.1': resolution: {integrity: sha512-YdXcMcEnqZhzql98RNrqYo9cEhTTesBiCclEtoiQUbJwx87q9453GTapYU6kJ8ZZ2ek1Vp25SiAXEFy5O/eAPw==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@mdx-js/react@3.1.0': resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@module-federation/error-codes@0.15.0': resolution: {integrity: sha512-CFJSF+XKwTcy0PFZ2l/fSUpR4z247+Uwzp1sXVkdIfJ/ATsnqf0Q01f51qqSEA6MYdQi6FKos9FIcu3dCpQNdg==} @@ -5135,8 +5135,8 @@ packages: '@storybook/blocks@8.6.0': resolution: {integrity: sha512-3PNxlB5Ooj8CIhttbDxeV6kW7ui+2GEdTngtqhnsUHVjzeTKpilsk2lviOeUzqlyq5FDK+rhpZ3L3DJ9pDvioA==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 storybook: ^8.6.0 peerDependenciesMeta: react: @@ -5186,8 +5186,8 @@ packages: resolution: {integrity: sha512-Nz/UzeYQdUZUhacrPyfkiiysSjydyjgg/p0P9HxB4p/WaJUUjMAcaoaLgy3EXx61zZJ3iD36WPuDkZs5QYrA0A==} engines: {node: '>=14.0.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 '@storybook/instrumenter@8.6.0': resolution: {integrity: sha512-eEY/Hfa3Vj5Nv4vHRHlSqjoyW6oAKNK3rKIXfL/eawQwb7rKhzijDLG5YBH44Hh7dEPIqUp0LEdgpyIY7GXezg==} @@ -5203,8 +5203,8 @@ packages: resolution: {integrity: sha512-04T86VG0UJtiozgZkTR5sY1qM3E0Rgwqwllvyy7kFFdkV+Sv/VsPjW9sC38s9C8FtCYRL8pJZz81ey3oylpIMA==} engines: {node: '>=18.0.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 storybook: ^8.6.0 typescript: '*' peerDependenciesMeta: @@ -5225,16 +5225,16 @@ packages: '@storybook/react-dom-shim@8.6.0': resolution: {integrity: sha512-5Y+vMHhcx0xnaNsLQMbkmjc3zkDn/fGBNsiLH2e4POvW3ZQvOxjoyxAsEQaKwLtFgsdCFSd2tR89F6ItYrA2JQ==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 storybook: ^8.6.0 '@storybook/react-webpack5@8.6.0': resolution: {integrity: sha512-2L9CYDPn1OL0B8K5EU/Wpo9Slg8f0vkYPaPioQnmcK3Q4SJR4JAuDVWHUtNdxhaPOkHIy887Tfrf6BEC/blMaQ==} engines: {node: '>=18.0.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 storybook: ^8.6.0 typescript: '>= 4.2.x' peerDependenciesMeta: @@ -5246,8 +5246,8 @@ packages: engines: {node: '>=18.0.0'} peerDependencies: '@storybook/test': 8.6.0 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 storybook: ^8.6.0 typescript: '>= 4.2.x' peerDependenciesMeta: @@ -5427,8 +5427,8 @@ packages: engines: {node: '>=18'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -11582,7 +11582,7 @@ packages: lucide-react@0.383.0: resolution: {integrity: sha512-13xlG0CQCJtzjSQYwwJ3WRqMHtRj3EXmLlorrARt7y+IHnxUCp3XyFNL1DfaGySWxHObDvnu1u1dV+0VMKHUSg==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} @@ -14112,23 +14112,23 @@ packages: resolution: {integrity: sha512-APPU8HB2uZnpl6Vt/+0AFoVYgSRtfiP6FLrZgPPTDmqSb2R4qZRbgd0A3VzIFxDt5e+Fozjx79WjLWnF69DK8g==} engines: {node: '>=16.14.0'} - react-dom@0.0.0-experimental-9784cb37-20250730: - resolution: {integrity: sha512-eUKSBDgp6cDKxtOVSMtu6gh85ZLZT2ZdzIWMtLma3CpJGfr3KK0NZN1Ccqu5YIQPUlzfDHkhwQa2EM4yDD6tJA==} + react-dom@0.0.0-experimental-c260b38d-20250731: + resolution: {integrity: sha512-bgh2tZ8U8+5RWxp1kOA02oXmJ9BqoDO5ASnO3er0Jq35Jre2qckRzAlLt6iYUG/eRSQjU86WU1T5aIFM/Leaow==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 - react-dom@19.2.0-canary-9784cb37-20250730: - resolution: {integrity: sha512-zUI83iYWnKvMc08OE6g6L3w6NwGDPtuHIEPtMGln3sTaznjKuXLEKihvNUWLXRQE1zxO2fhnHYaeYge0vRP+jg==} + react-dom@19.2.0-canary-c260b38d-20250731: + resolution: {integrity: sha512-yfUUHKacH3GZEcmrjQW6hPPl7r0TdAchNq9ZXXUc+9TuxeYdojCSBWUpHPibr8sK9m4Zj+8ptBpTKw3P/e/Bdg==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 react-dom@19.2.0-canary-eaee5308-20250728: resolution: {integrity: sha512-MVLdI3bsHF962avEiCIMIQ8yO6p6cRIGGTeBl4K1Lw3Yq/KsFxkpRbBqV4C1DVoDe5EXIGDezoTEPDpZcRFUow==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 - react-is@19.2.0-canary-9784cb37-20250730: - resolution: {integrity: sha512-S3NME+2fOenIWI20q6j0cptwaOrzPLSbv6v2HNAicdxiNb6i/bL/lJxXByo5LvzO/+BW5pL5MwwS6skUy1e3Fg==} + react-is@19.2.0-canary-c260b38d-20250731: + resolution: {integrity: sha512-fGg/SDYF7Ny2zGrqCtnvhGIzAQxl0MFAh0zI5UivYNefv6Fr96cCI7606pGtdkWSDhnorpw760S9ErXnw2mLrQ==} react-is@19.2.0-canary-eaee5308-20250728: resolution: {integrity: sha512-kaeTCRQJulmP27MCI7/AEiWE9c0lRCIGumFD9A9NXvxtBR1QDHcPLJDQ7B8A3we9uuJJhW5iY2IrCHWERX194w==} @@ -14139,8 +14139,8 @@ packages: react-number-format@5.4.0: resolution: {integrity: sha512-NWdICrqLhI7rAS8yUeLVd6Wr4cN7UjJ9IBTS0f/a9i7UB4x4Ti70kGnksBtZ7o4Z7YRbvCMMR/jQmkoOBa/4fg==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 react-refresh@0.12.0: resolution: {integrity: sha512-suLIhrU2IHKL5JEKR/fAwJv7bbeq4kJ+pJopf77jHwuR+HmJS/HbrPIGsTBUVfw7tXPOmYv7UJ7PCaN49e8x4A==} @@ -14151,7 +14151,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -14161,58 +14161,58 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true - react-server-dom-turbopack@0.0.0-experimental-9784cb37-20250730: - resolution: {integrity: sha512-CNNJZHiiKdo7G+WNMjSuGknSCU+B1+lDkcQ2/WpQJHIrIlwp+N2KB2NuujXt6qrxotEh8PAK8ajGiZ8flHPNZg==} + react-server-dom-turbopack@0.0.0-experimental-c260b38d-20250731: + resolution: {integrity: sha512-03+RB4xYkWqrPSBm9YaP5zl5BE6aKpKVcj814eG0LtXEc1/g5Sg2LqnIX3+bboUiGz51qW4fBvkm8XoDtIloAA==} engines: {node: '>=0.10.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 - react-server-dom-turbopack@19.2.0-canary-9784cb37-20250730: - resolution: {integrity: sha512-EX0OaHLQ3m19zFiP2YfFiZ4rq1ZK7X12RXlE3TIIsCV2EjwRHfPnXfItcG8GPlu+fLa7iJbYtB/fRxZB7xCh+Q==} + react-server-dom-turbopack@19.2.0-canary-c260b38d-20250731: + resolution: {integrity: sha512-kInYGZSX7r0X4IoIO1puItGX1gU+OejE9jgb+YgzJv4IkaNrY7bis8HKjYuJlclzk1YrHX800mK6LyTOWgbbBA==} engines: {node: '>=0.10.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 - react-server-dom-webpack@0.0.0-experimental-9784cb37-20250730: - resolution: {integrity: sha512-xE2hFbTMY9+yVAmz5ehf3SBycKBoh75GERDIJkn9inehHh+gfsa3fUuXPVLYodKDHSY2nzlJuRtteDRlWjJM5w==} + react-server-dom-webpack@0.0.0-experimental-c260b38d-20250731: + resolution: {integrity: sha512-S+rlCN72BtT5Untk8VSQoRHF/yDTVIE6rXEKv13jOzQUzvV6C318iyhQcHrdX5+LTiKi0NM4WxRPD7Hq7AuGvw==} engines: {node: '>=0.10.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 webpack: 5.98.0 - react-server-dom-webpack@19.2.0-canary-9784cb37-20250730: - resolution: {integrity: sha512-PcMeD21EW9Tku3ZmYIoGWFBP1smRW1d4Zi/jXnlZ04xp+Q/zfANAxKHwGFnyyK0oCeUYkUOt2X4MBAPxKhXX0g==} + react-server-dom-webpack@19.2.0-canary-c260b38d-20250731: + resolution: {integrity: sha512-u9Yt9NQWwahXb2V5gu/WMZINnun8Srdn/twhJJQBiA9aY9iPCfY4Carzc4MOy32jmgNncHS5GtqmqaXuR0MFzw==} engines: {node: '>=0.10.0'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 webpack: 5.98.0 react-shallow-renderer@16.15.0: resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 react-ssr-prepass@1.0.8: resolution: {integrity: sha512-O0gfRA1SaK+9ITKxqfnXsej2jF+OHGP/+GxD4unROQaM/0/UczGF9fuF+wTboxaQoKdIf4FvS3h/OigWh704VA==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-is: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-is: 19.2.0-canary-c260b38d-20250731 react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -14220,26 +14220,26 @@ packages: react-test-renderer@18.2.0: resolution: {integrity: sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 react-textarea-autosize@8.5.3: resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} engines: {node: '>=10'} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 react-virtualized@9.22.3: resolution: {integrity: sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731 - react@0.0.0-experimental-9784cb37-20250730: - resolution: {integrity: sha512-iivIZqF1Wfi7INr6VcRiLo5/iDbOFvHNm9ANJFcPNqIz1aJcKhA7CGzo3siaHVV2woHFtfQQWJHAhtKF5jN4Fg==} + react@0.0.0-experimental-c260b38d-20250731: + resolution: {integrity: sha512-WRvBwTa6Ym++y5dH5HSiDss3MvnDD1aLE1PGRnOxlt7q/z5DSeIgkIep4TJrcj7iezkY9AOVvnFs6OksL4Yy6w==} engines: {node: '>=0.10.0'} - react@19.2.0-canary-9784cb37-20250730: - resolution: {integrity: sha512-AJBTmlZf0OGKIL7xOsIfZUu//Jtz7BPqSOeGw7dap7AkaYyHvsEeFy96F9Hsgm/18zjx3mdq8heJmGkYrIJQTg==} + react@19.2.0-canary-c260b38d-20250731: + resolution: {integrity: sha512-33+NpNopfnwgW4D5IqErXXrBHm6AB7GSWjnBlDQ+3d1JH5jFwfPxLzm+05ZmhYYDD6W03e52jSZtiTPSJhy98w==} engines: {node: '>=0.10.0'} react@19.2.0-canary-eaee5308-20250728: @@ -14801,11 +14801,11 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.0.0-experimental-9784cb37-20250730: - resolution: {integrity: sha512-z8/SiNnxs/gSjS/3ksN1z2eqaS1tJfc+aBMxukNzV2TUq7pGwwzpVr/0w3lNDVPUY3fnnHUaG1KJppJOrjepGw==} + scheduler@0.0.0-experimental-c260b38d-20250731: + resolution: {integrity: sha512-2tju1PBtrfzZKeokX9Lg4XvKw7Zu6xHjI3hUXi+4HWvuSN45bpavyQePTS026MRDX4oScBxY9AB5eo1gJInTVQ==} - scheduler@0.27.0-canary-9784cb37-20250730: - resolution: {integrity: sha512-wwpg7hRameT9pQed/7x+DaZ8Ui1bwAeJuxMjwcCKvYqECvG09LCoHEUzcNYWC7ITCERGTdtz1hbELAL4it1lCw==} + scheduler@0.27.0-canary-c260b38d-20250731: + resolution: {integrity: sha512-Fq5M0ZSZsLfBjEYd8f56JMNpKUpqtFqt0tugLxW6V8aru+/RlN0zm9Bl0J4B+/u5drlNI77X8HvpLh6fU3unfw==} schema-utils@2.7.1: resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} @@ -15464,7 +15464,7 @@ packages: peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@babel/core': optional: true @@ -15545,7 +15545,7 @@ packages: swr@2.2.4: resolution: {integrity: sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 symbol-observable@1.0.1: resolution: {integrity: sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw==} @@ -16337,7 +16337,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -16345,13 +16345,13 @@ packages: use-composed-ref@1.3.0: resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 use-isomorphic-layout-effect@1.1.2: resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: '@types/react': '*' - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -16360,7 +16360,7 @@ packages: resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} peerDependencies: '@types/react': '*' - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -16370,7 +16370,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 peerDependenciesMeta: '@types/react': optional: true @@ -16378,12 +16378,12 @@ packages: use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -18073,28 +18073,28 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@base-ui-components/react@1.0.0-beta.2(@types/react@19.1.8)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)': + '@base-ui-components/react@1.0.0-beta.2(@types/react@19.1.8)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@babel/runtime': 7.27.6 - '@base-ui-components/utils': 0.1.0(@types/react@19.1.8)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) - '@floating-ui/react-dom': 2.1.5(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + '@base-ui-components/utils': 0.1.0(@types/react@19.1.8)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) + '@floating-ui/react-dom': 2.1.5(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) '@floating-ui/utils': 0.2.10 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) reselect: 5.1.1 tabbable: 6.2.0 - use-sync-external-store: 1.5.0(react@19.2.0-canary-9784cb37-20250730) + use-sync-external-store: 1.5.0(react@19.2.0-canary-c260b38d-20250731) optionalDependencies: '@types/react': 19.1.8 - '@base-ui-components/utils@0.1.0(@types/react@19.1.8)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)': + '@base-ui-components/utils@0.1.0(@types/react@19.1.8)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@babel/runtime': 7.27.6 '@floating-ui/utils': 0.2.10 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) reselect: 5.1.1 - use-sync-external-store: 1.5.0(react@19.2.0-canary-9784cb37-20250730) + use-sync-external-store: 1.5.0(react@19.2.0-canary-c260b38d-20250731) optionalDependencies: '@types/react': 19.1.8 @@ -18427,17 +18427,17 @@ snapshots: '@emotion/memoize@0.8.1': {} - '@emotion/react@11.11.1(@types/react@19.1.8)(react@19.2.0-canary-9784cb37-20250730)': + '@emotion/react@11.11.1(@types/react@19.1.8)(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@babel/runtime': 7.27.0 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 '@emotion/serialize': 1.1.2 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.2.0-canary-9784cb37-20250730) + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.2.0-canary-c260b38d-20250731) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 hoist-non-react-statics: 3.3.2 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 optionalDependencies: '@types/react': 19.1.8 transitivePeerDependencies: @@ -18455,9 +18455,9 @@ snapshots: '@emotion/unitless@0.8.1': {} - '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@19.2.0-canary-9784cb37-20250730)': + '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@19.2.0-canary-c260b38d-20250731)': dependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@emotion/utils@1.2.1': {} @@ -18981,11 +18981,11 @@ snapshots: react: 19.2.0-canary-eaee5308-20250728 react-dom: 19.2.0-canary-eaee5308-20250728(react@19.2.0-canary-eaee5308-20250728) - '@floating-ui/react-dom@2.1.5(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)': + '@floating-ui/react-dom@2.1.5(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@floating-ui/dom': 1.7.3 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) '@floating-ui/react@0.26.16(react-dom@19.2.0-canary-eaee5308-20250728(react@19.2.0-canary-eaee5308-20250728))(react@19.2.0-canary-eaee5308-20250728)': dependencies: @@ -20144,11 +20144,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@2.2.1(react@19.2.0-canary-9784cb37-20250730)': + '@mdx-js/react@2.2.1(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@types/mdx': 2.0.3 '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@mdx-js/react@2.2.1(react@19.2.0-canary-eaee5308-20250728)': dependencies: @@ -20156,11 +20156,11 @@ snapshots: '@types/react': 19.1.8 react: 19.2.0-canary-eaee5308-20250728 - '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.2.0-canary-9784cb37-20250730)': + '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@types/mdx': 2.0.3 '@types/react': 19.1.8 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 '@module-federation/error-codes@0.15.0': {} @@ -20920,12 +20920,12 @@ snapshots: '@storybook/addon-docs@8.6.0(@types/react@19.1.8)(storybook@8.6.0(prettier@3.3.3))': dependencies: - '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.2.0-canary-9784cb37-20250730) - '@storybook/blocks': 8.6.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3)) + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.2.0-canary-c260b38d-20250731) + '@storybook/blocks': 8.6.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3)) '@storybook/csf-plugin': 8.6.0(storybook@8.6.0(prettier@3.3.3)) - '@storybook/react-dom-shim': 8.6.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3)) - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + '@storybook/react-dom-shim': 8.6.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3)) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) storybook: 8.6.0(prettier@3.3.3) ts-dedent: 2.2.0 transitivePeerDependencies: @@ -20990,14 +20990,14 @@ snapshots: - '@swc/helpers' - webpack - '@storybook/blocks@8.6.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))': + '@storybook/blocks@8.6.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))': dependencies: - '@storybook/icons': 1.3.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730) + '@storybook/icons': 1.3.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731) storybook: 8.6.0(prettier@3.3.3) ts-dedent: 2.2.0 optionalDependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) '@storybook/builder-webpack5@8.6.0(@rspack/core@1.4.5(@swc/helpers@0.5.15))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': dependencies: @@ -21076,10 +21076,10 @@ snapshots: '@storybook/global@5.0.0': {} - '@storybook/icons@1.3.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)': + '@storybook/icons@1.3.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)': dependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) '@storybook/instrumenter@8.6.0(storybook@8.6.0(prettier@3.3.3))': dependencies: @@ -21091,17 +21091,17 @@ snapshots: dependencies: storybook: 8.6.0(prettier@3.3.3) - '@storybook/preset-react-webpack@8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': + '@storybook/preset-react-webpack@8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': dependencies: '@storybook/core-webpack': 8.6.0(storybook@8.6.0(prettier@3.3.3)) - '@storybook/react': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + '@storybook/react': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.2)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)) '@types/semver': 7.5.6 find-up: 5.0.0 magic-string: 0.30.17 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 react-docgen: 7.1.0 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) resolve: 1.22.10 semver: 7.6.3 storybook: 8.6.0(prettier@3.3.3) @@ -21135,19 +21135,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@8.6.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))': + '@storybook/react-dom-shim@8.6.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))': dependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) storybook: 8.6.0(prettier@3.3.3) - '@storybook/react-webpack5@8.6.0(@rspack/core@1.4.5(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': + '@storybook/react-webpack5@8.6.0(@rspack/core@1.4.5(@swc/helpers@0.5.15))(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': dependencies: '@storybook/builder-webpack5': 8.6.0(@rspack/core@1.4.5(@swc/helpers@0.5.15))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) - '@storybook/preset-react-webpack': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) - '@storybook/react': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + '@storybook/preset-react-webpack': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + '@storybook/react': 8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) storybook: 8.6.0(prettier@3.3.3) optionalDependencies: typescript: 5.8.2 @@ -21160,16 +21160,16 @@ snapshots: - uglify-js - webpack-cli - '@storybook/react@8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': + '@storybook/react@8.6.0(@storybook/test@8.6.0(storybook@8.6.0(prettier@3.3.3)))(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3))(typescript@5.8.2)': dependencies: '@storybook/components': 8.6.0(storybook@8.6.0(prettier@3.3.3)) '@storybook/global': 5.0.0 '@storybook/manager-api': 8.6.0(storybook@8.6.0(prettier@3.3.3)) '@storybook/preview-api': 8.6.0(storybook@8.6.0(prettier@3.3.3)) - '@storybook/react-dom-shim': 8.6.0(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(storybook@8.6.0(prettier@3.3.3)) + '@storybook/react-dom-shim': 8.6.0(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(storybook@8.6.0(prettier@3.3.3)) '@storybook/theming': 8.6.0(storybook@8.6.0(prettier@3.3.3)) - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) storybook: 8.6.0(prettier@3.3.3) optionalDependencies: '@storybook/test': 8.6.0(storybook@8.6.0(prettier@3.3.3)) @@ -21376,13 +21376,13 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@15.0.7(@types/react@19.1.8)(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)': + '@testing-library/react@15.0.7(@types/react@19.1.8)(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)': dependencies: '@babel/runtime': 7.27.0 '@testing-library/dom': 10.1.0 '@types/react-dom': 19.1.6(@types/react@19.1.8) - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) optionalDependencies: '@types/react': 19.1.8 @@ -27181,7 +27181,7 @@ snapshots: hoist-non-react-statics@3.3.2: dependencies: - react-is: 19.2.0-canary-9784cb37-20250730 + react-is: 19.2.0-canary-c260b38d-20250731 homedir-polyfill@1.0.3: dependencies: @@ -32122,25 +32122,25 @@ snapshots: dependencies: ansi-regex: 5.0.1 ansi-styles: 5.2.0 - react-is: 19.2.0-canary-9784cb37-20250730 + react-is: 19.2.0-canary-c260b38d-20250731 pretty-format@29.5.0: dependencies: '@jest/schemas': 29.4.3 ansi-styles: 5.2.0 - react-is: 19.2.0-canary-9784cb37-20250730 + react-is: 19.2.0-canary-c260b38d-20250731 pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 19.2.0-canary-9784cb37-20250730 + react-is: 19.2.0-canary-c260b38d-20250731 pretty-format@30.0.0-alpha.6: dependencies: '@jest/schemas': 30.0.0-alpha.6 ansi-styles: 5.2.0 - react-is: 19.2.0-canary-9784cb37-20250730 + react-is: 19.2.0-canary-c260b38d-20250731 pretty-ms@7.0.0: dependencies: @@ -32203,7 +32203,7 @@ snapshots: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 - react-is: 19.2.0-canary-9784cb37-20250730 + react-is: 19.2.0-canary-c260b38d-20250731 property-information@5.6.0: dependencies: @@ -32414,22 +32414,22 @@ snapshots: transitivePeerDependencies: - supports-color - react-dom@0.0.0-experimental-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730): + react-dom@0.0.0-experimental-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731): dependencies: - react: 19.2.0-canary-9784cb37-20250730 - scheduler: 0.27.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + scheduler: 0.27.0-canary-c260b38d-20250731 - react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730): + react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731): dependencies: - react: 19.2.0-canary-9784cb37-20250730 - scheduler: 0.27.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + scheduler: 0.27.0-canary-c260b38d-20250731 react-dom@19.2.0-canary-eaee5308-20250728(react@19.2.0-canary-eaee5308-20250728): dependencies: react: 19.2.0-canary-eaee5308-20250728 - scheduler: 0.27.0-canary-9784cb37-20250730 + scheduler: 0.27.0-canary-c260b38d-20250731 - react-is@19.2.0-canary-9784cb37-20250730: {} + react-is@19.2.0-canary-c260b38d-20250731: {} react-is@19.2.0-canary-eaee5308-20250728: {} @@ -32462,48 +32462,48 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - react-server-dom-turbopack@0.0.0-experimental-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730): + react-server-dom-turbopack@0.0.0-experimental-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731): dependencies: acorn-loose: 8.3.0 neo-async: 2.6.1 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) - react-server-dom-turbopack@19.2.0-canary-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730): + react-server-dom-turbopack@19.2.0-canary-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731): dependencies: acorn-loose: 8.3.0 neo-async: 2.6.1 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) - react-server-dom-webpack@0.0.0-experimental-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))): + react-server-dom-webpack@0.0.0-experimental-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))): dependencies: acorn-loose: 8.3.0 neo-async: 2.6.1 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) webpack: 5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15)) webpack-sources: 3.2.3(patch_hash=jbynf5dc46ambamq3wuyho6hkq) - react-server-dom-webpack@19.2.0-canary-9784cb37-20250730(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))): + react-server-dom-webpack@19.2.0-canary-c260b38d-20250731(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731)(webpack@5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))): dependencies: acorn-loose: 8.3.0 neo-async: 2.6.1 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) webpack: 5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15)) webpack-sources: 3.2.3(patch_hash=jbynf5dc46ambamq3wuyho6hkq) - react-shallow-renderer@16.15.0(react@19.2.0-canary-9784cb37-20250730): + react-shallow-renderer@16.15.0(react@19.2.0-canary-c260b38d-20250731): dependencies: object-assign: 4.1.1 - react: 19.2.0-canary-9784cb37-20250730 - react-is: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-is: 19.2.0-canary-c260b38d-20250731 - react-ssr-prepass@1.0.8(react-is@19.2.0-canary-eaee5308-20250728)(react@19.2.0-canary-9784cb37-20250730): + react-ssr-prepass@1.0.8(react-is@19.2.0-canary-eaee5308-20250728)(react@19.2.0-canary-c260b38d-20250731): dependencies: object-is: 1.0.2 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 react-is: 19.2.0-canary-eaee5308-20250728 react-style-singleton@2.2.1(@types/react@19.1.8)(react@19.2.0-canary-eaee5308-20250728): @@ -32515,12 +32515,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - react-test-renderer@18.2.0(react@19.2.0-canary-9784cb37-20250730): + react-test-renderer@18.2.0(react@19.2.0-canary-c260b38d-20250731): dependencies: - react: 19.2.0-canary-9784cb37-20250730 - react-is: 19.2.0-canary-9784cb37-20250730 - react-shallow-renderer: 16.15.0(react@19.2.0-canary-9784cb37-20250730) - scheduler: 0.27.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 + react-is: 19.2.0-canary-c260b38d-20250731 + react-shallow-renderer: 16.15.0(react@19.2.0-canary-c260b38d-20250731) + scheduler: 0.27.0-canary-c260b38d-20250731 react-textarea-autosize@8.5.3(@types/react@19.1.8)(react@19.2.0-canary-eaee5308-20250728): dependencies: @@ -32531,20 +32531,20 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-virtualized@9.22.3(react-dom@19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730))(react@19.2.0-canary-9784cb37-20250730): + react-virtualized@9.22.3(react-dom@19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731))(react@19.2.0-canary-c260b38d-20250731): dependencies: '@babel/runtime': 7.27.0 clsx: 1.1.1 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.0-canary-9784cb37-20250730 - react-dom: 19.2.0-canary-9784cb37-20250730(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + react-dom: 19.2.0-canary-c260b38d-20250731(react@19.2.0-canary-c260b38d-20250731) react-lifecycles-compat: 3.0.4 - react@0.0.0-experimental-9784cb37-20250730: {} + react@0.0.0-experimental-c260b38d-20250731: {} - react@19.2.0-canary-9784cb37-20250730: {} + react@19.2.0-canary-c260b38d-20250731: {} react@19.2.0-canary-eaee5308-20250728: {} @@ -33314,9 +33314,9 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.0.0-experimental-9784cb37-20250730: {} + scheduler@0.0.0-experimental-c260b38d-20250731: {} - scheduler@0.27.0-canary-9784cb37-20250730: {} + scheduler@0.27.0-canary-c260b38d-20250731: {} schema-utils@2.7.1: dependencies: @@ -34117,10 +34117,10 @@ snapshots: postcss: 7.0.32 postcss-load-plugins: 2.3.0 - styled-jsx@5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-9784cb37-20250730): + styled-jsx@5.1.6(@babel/core@7.26.10)(babel-plugin-macros@3.1.0)(react@19.2.0-canary-c260b38d-20250731): dependencies: client-only: 0.0.1 - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 optionalDependencies: '@babel/core': 7.26.10 babel-plugin-macros: 3.1.0 @@ -34214,11 +34214,11 @@ snapshots: '@swc/counter': 0.1.3 webpack: 5.98.0(@swc/core@1.11.24(@swc/helpers@0.5.15))(esbuild@0.24.2) - swr@2.2.4(react@19.2.0-canary-9784cb37-20250730): + swr@2.2.4(react@19.2.0-canary-c260b38d-20250731): dependencies: client-only: 0.0.1 - react: 19.2.0-canary-9784cb37-20250730 - use-sync-external-store: 1.2.0(react@19.2.0-canary-9784cb37-20250730) + react: 19.2.0-canary-c260b38d-20250731 + use-sync-external-store: 1.2.0(react@19.2.0-canary-c260b38d-20250731) symbol-observable@1.0.1: {} @@ -35015,9 +35015,9 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - unistore@3.4.1(react@19.2.0-canary-9784cb37-20250730): + unistore@3.4.1(react@19.2.0-canary-c260b38d-20250731): optionalDependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 universal-github-app-jwt@1.1.1: dependencies: @@ -35143,13 +35143,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - use-sync-external-store@1.2.0(react@19.2.0-canary-9784cb37-20250730): + use-sync-external-store@1.2.0(react@19.2.0-canary-c260b38d-20250731): dependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 - use-sync-external-store@1.5.0(react@19.2.0-canary-9784cb37-20250730): + use-sync-external-store@1.5.0(react@19.2.0-canary-c260b38d-20250731): dependencies: - react: 19.2.0-canary-9784cb37-20250730 + react: 19.2.0-canary-c260b38d-20250731 util-deprecate@1.0.2: {} diff --git a/test/e2e/app-dir/searchparams-reuse-loading/app/page.tsx b/test/e2e/app-dir/searchparams-reuse-loading/app/page.tsx index 5cc9a6f30ef542..ea4e9e44440b83 100644 --- a/test/e2e/app-dir/searchparams-reuse-loading/app/page.tsx +++ b/test/e2e/app-dir/searchparams-reuse-loading/app/page.tsx @@ -16,13 +16,13 @@ export default async function Page(props) { /search-params?id=2
  • - - /search-params?id=3 (prefetch: true) + + /search-params?id=3 (prefetch: unstable_forceStale)
  • - - /search-params (prefetch: true) + + /search-params (prefetch: unstable_forceStale)
  • diff --git a/test/e2e/app-dir/searchparams-reuse-loading/app/with-middleware/page.tsx b/test/e2e/app-dir/searchparams-reuse-loading/app/with-middleware/page.tsx index 890256e1d024aa..f62bc8039bfe74 100644 --- a/test/e2e/app-dir/searchparams-reuse-loading/app/with-middleware/page.tsx +++ b/test/e2e/app-dir/searchparams-reuse-loading/app/with-middleware/page.tsx @@ -5,7 +5,7 @@ export default function Page() {
    • - /search-params?id=1 (prefetch: true) + /search-params?id=1
    • @@ -14,12 +14,18 @@ export default function Page() {
    • - + /search-params?id=3 (prefetch: true)
    • - + /search-params (prefetch: true)
    • diff --git a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts index aca58a6dee5314..ff8070c83b1cae 100644 --- a/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts +++ b/test/e2e/app-dir/segment-cache/cdn-cache-busting/cdn-cache-busting.test.ts @@ -11,6 +11,8 @@ describe('segment cache (CDN cache busting)', () => { return } + // TODO(runtime-ppr): add tests for runtime prefetches + // To debug these tests locally, run: // node start.mjs // diff --git a/test/e2e/app-dir/segment-cache/cdn-cache-busting/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/cdn-cache-busting/components/link-accordion.tsx index c735a55918b548..c6848d479aef8c 100644 --- a/test/e2e/app-dir/segment-cache/cdn-cache-busting/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/cdn-cache-busting/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -10,7 +10,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] }) { const [isVisible, setIsVisible] = useState(false) return ( diff --git a/test/e2e/app-dir/segment-cache/client-only-opt-in/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/client-only-opt-in/components/link-accordion.tsx index c735a55918b548..c6848d479aef8c 100644 --- a/test/e2e/app-dir/segment-cache/client-only-opt-in/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/client-only-opt-in/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -10,7 +10,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] }) { const [isVisible, setIsVisible] = useState(false) return ( diff --git a/test/e2e/app-dir/segment-cache/deployment-skew/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/deployment-skew/components/link-accordion.tsx index c735a55918b548..c6848d479aef8c 100644 --- a/test/e2e/app-dir/segment-cache/deployment-skew/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/deployment-skew/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -10,7 +10,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] }) { const [isVisible, setIsVisible] = useState(false) return ( diff --git a/test/e2e/app-dir/segment-cache/dynamic-on-hover/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/dynamic-on-hover/components/link-accordion.tsx index 7b9fc4520533d5..4aa5ddff9ebcba 100644 --- a/test/e2e/app-dir/segment-cache/dynamic-on-hover/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/dynamic-on-hover/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -11,7 +11,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] unstable_dynamicOnHover?: boolean }) { const [isVisible, setIsVisible] = useState(false) diff --git a/test/e2e/app-dir/segment-cache/incremental-opt-in/app/link-accordion.tsx b/test/e2e/app-dir/segment-cache/incremental-opt-in/app/link-accordion.tsx index 2a1117ad313b4f..40e57151e30551 100644 --- a/test/e2e/app-dir/segment-cache/incremental-opt-in/app/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/incremental-opt-in/app/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -11,7 +11,7 @@ export function LinkAccordion({ }: { href: string children: string - prefetch?: boolean + prefetch?: LinkProps['prefetch'] id?: string }) { const [isVisible, setIsVisible] = useState(false) diff --git a/test/e2e/app-dir/segment-cache/incremental-opt-in/app/mixed-fetch-strategies/page.tsx b/test/e2e/app-dir/segment-cache/incremental-opt-in/app/mixed-fetch-strategies/page.tsx index 629599ce81da60..bc02ad99ecf631 100644 --- a/test/e2e/app-dir/segment-cache/incremental-opt-in/app/mixed-fetch-strategies/page.tsx +++ b/test/e2e/app-dir/segment-cache/incremental-opt-in/app/mixed-fetch-strategies/page.tsx @@ -22,10 +22,10 @@ export default function MixedFetchStrategies() {
    • - Same link, but with prefetch=true + Same link, but with prefetch="unstable_forceStale"
    diff --git a/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts b/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts index 9927f75c522c16..0ec9849e6ed8b6 100644 --- a/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts +++ b/test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts @@ -285,7 +285,7 @@ describe('segment cache (incremental opt in)', () => { ) it( - 'when a link is prefetched with , no dynamic request ' + + 'when a link is prefetched with , no dynamic request ' + 'is made on navigation', async () => { let act @@ -324,7 +324,7 @@ describe('segment cache (incremental opt in)', () => { ) it( - 'when prefetching with prefetch=true, refetches cache entries that only ' + + 'when prefetching with prefetch="unstable_forceStale", refetches cache entries that only ' + 'contain partial data', async () => { let act @@ -343,7 +343,7 @@ describe('segment cache (incremental opt in)', () => { { includes: 'Loading (PPR shell of shared-layout)...' } ) - // Prefetch the same link again, this time with prefetch=true to include + // Prefetch the same link again, this time with prefetch="unstable_forceStale" to include // the dynamic data await act( async () => { @@ -370,7 +370,7 @@ describe('segment cache (incremental opt in)', () => { // If this fails, it likely means that the partial cache entry that // resulted from prefetching the normal link () // was not properly re-fetched when the full link () was prefetched. + // prefetch='unstable_forceStale'>) was prefetched. await browser.elementById('page-content') }, // Assert that no network requests are initiated within this block. @@ -380,7 +380,7 @@ describe('segment cache (incremental opt in)', () => { ) it( - 'when prefetching with prefetch=true, refetches partial cache entries ' + + 'when prefetching with prefetch="unstable_forceStale", refetches partial cache entries ' + "even if there's already a pending PPR request", async () => { // This test is hard to describe succinctly because it involves a fairly @@ -427,7 +427,7 @@ describe('segment cache (incremental opt in)', () => { ) // Before the previous prefetch finishes, prefetch the same link again, - // this time with prefetch=true to include the dynamic data. + // this time with prefetch="unstable_forceStale" to include the dynamic data. await act( async () => { const checkbox = await browser.elementById( @@ -457,7 +457,7 @@ describe('segment cache (incremental opt in)', () => { // If this fails, it likely means that the pending cache entry that // resulted from prefetching the normal link () // was not properly re-fetched when the full link () was prefetched. + // prefetch='unstable_forceStale'>) was prefetched. await browser.elementById('page-content') }, // Assert that no network requests are initiated within this block. diff --git a/test/e2e/app-dir/segment-cache/metadata/app/link-accordion.tsx b/test/e2e/app-dir/segment-cache/metadata/app/link-accordion.tsx index 8b2c95b9c00237..40e57151e30551 100644 --- a/test/e2e/app-dir/segment-cache/metadata/app/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/metadata/app/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -11,7 +11,7 @@ export function LinkAccordion({ }: { href: string children: string - prefetch?: boolean | 'unstable_forceStale' | 'auto' + prefetch?: LinkProps['prefetch'] id?: string }) { const [isVisible, setIsVisible] = useState(false) @@ -25,7 +25,6 @@ export function LinkAccordion({ id={id} /> {isVisible ? ( - // @ts-expect-error - unstable_forceStale is not yet part of the types {children} diff --git a/test/e2e/app-dir/segment-cache/metadata/app/page-with-runtime-prefetchable-head/page.tsx b/test/e2e/app-dir/segment-cache/metadata/app/page-with-runtime-prefetchable-head/page.tsx new file mode 100644 index 00000000000000..7c89517c848adb --- /dev/null +++ b/test/e2e/app-dir/segment-cache/metadata/app/page-with-runtime-prefetchable-head/page.tsx @@ -0,0 +1,23 @@ +import { Metadata } from 'next' +import { Suspense } from 'react' +import { cookies } from 'next/headers' + +export async function generateMetadata(): Promise { + await cookies() + return { + title: 'Runtime-prefetchable title', + } +} + +async function Content() { + await cookies() + return
    Target page
    +} + +export default function PageWithRuntimePrefetchableTitle() { + return ( + + + + ) +} diff --git a/test/e2e/app-dir/segment-cache/metadata/app/page.tsx b/test/e2e/app-dir/segment-cache/metadata/app/page.tsx index df8dd56e3a7caf..99d91b37e35b1e 100644 --- a/test/e2e/app-dir/segment-cache/metadata/app/page.tsx +++ b/test/e2e/app-dir/segment-cache/metadata/app/page.tsx @@ -5,16 +5,38 @@ export default function Page() { <>
    • - - Page with dynamic head (prefetch=true)) + + Page with dynamic head (prefetch=unstable_forceStale)
    • - Rewrite to page with dynamic head (prefetch=true) + Rewrite to page with dynamic head (prefetch=unstable_forceStale) + +
    • +
    +
    +
      +
    • + + Page with runtime-prefetchable head (prefetch=true) + +
    • +
    • + + Rewrite to page with runtime-prefetchable head (prefetch=true)
    diff --git a/test/e2e/app-dir/segment-cache/metadata/next.config.js b/test/e2e/app-dir/segment-cache/metadata/next.config.js index e747e503ad0a21..669695f920dc57 100644 --- a/test/e2e/app-dir/segment-cache/metadata/next.config.js +++ b/test/e2e/app-dir/segment-cache/metadata/next.config.js @@ -12,6 +12,10 @@ const nextConfig = { source: '/rewrite-to-page-with-dynamic-head', destination: '/page-with-dynamic-head', }, + { + source: '/rewrite-to-page-with-runtime-prefetchable-head', + destination: '/page-with-runtime-prefetchable-head', + }, ] }, } diff --git a/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts b/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts index 4d9300e3982cd5..2866e499a38ec5 100644 --- a/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts +++ b/test/e2e/app-dir/segment-cache/metadata/segment-cache-metadata.test.ts @@ -9,12 +9,9 @@ describe('segment cache (metadata)', () => { test('disabled in development', () => {}) return } - - it( - "regression: prefetch the head if it's missing even if all other data " + - 'is cached', - async () => { - let act + describe("regression: prefetch the head if it's missing even if all other data is cached", () => { + it('pages with dynamic content and dynamic metadata, using a full prefetch', async () => { + let act: ReturnType const browser = await next.browser('/', { beforePageLoad(p) { act = createRouterAct(p) @@ -75,6 +72,71 @@ describe('segment cache (metadata)', () => { const title = await browser.eval(() => document.title) expect(title).toBe('Dynamic Title') }, 'no-requests') - } - ) + }) + + it('pages with runtime-prefetchable content and dynamic metadata, using a runtime prefetch', async () => { + let act: ReturnType + const browser = await next.browser('/', { + beforePageLoad(p) { + act = createRouterAct(p) + }, + }) + + // Runtime-prefetch a page. + // It only uses cookies, so this should be a complete prefetch. + await act(async () => { + const checkbox = await browser.elementByCss( + 'input[data-link-accordion="/page-with-runtime-prefetchable-head"]' + ) + await checkbox.click() + }, [ + // Because the link is prefetched with prefetch="unstable_forceStale", + // we should be able to prefetch the title, even though it's dynamic. + { + includes: 'Runtime-prefetchable title', + }, + { + includes: 'Target page', + }, + ]) + + // Now runtime-prefetch a link that rewrites to the same underlying page. + await act(async () => { + const checkbox = await browser.elementByCss( + 'input[data-link-accordion="/rewrite-to-page-with-runtime-prefetchable-head"]' + ) + await checkbox.click() + }, [ + // TODO: Ideally, this would not prefetch the dynamic title again, + // because it was already prefetched by the previous link, and both + // links resolve to the same underlying route. This is because, unlike + // segment data, we cache routes solely by their input URL, not by the + // path of the underlying route. Similarly, we don't cache metadata + // separately from the route tree. We should probably do one or both. + { + includes: 'Runtime-prefetchable title', + }, + // It should not prefetch the page content again, because it was + // already cached. + { + includes: 'Target page', + block: 'reject', + }, + ]) + + // When we navigate to the page, it should not make any additional + // network requests, because both the segment data and the head were + // fully prefetched. + await act(async () => { + const link = await browser.elementByCss( + 'a[href="/rewrite-to-page-with-runtime-prefetchable-head"]' + ) + await link.click() + const pageContent = await browser.elementById('target-page') + expect(await pageContent.text()).toBe('Target page') + const title = await browser.eval(() => document.title) + expect(title).toBe('Runtime-prefetchable title') + }, 'no-requests') + }) + }) }) diff --git a/test/e2e/app-dir/segment-cache/mpa-navigations/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/mpa-navigations/components/link-accordion.tsx index c735a55918b548..c6848d479aef8c 100644 --- a/test/e2e/app-dir/segment-cache/mpa-navigations/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/mpa-navigations/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -10,7 +10,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] }) { const [isVisible, setIsVisible] = useState(false) return ( diff --git a/test/e2e/app-dir/segment-cache/prefetch-auto/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/prefetch-auto/components/link-accordion.tsx index 064d7ff73f50d8..191aeead41903b 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-auto/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/prefetch-auto/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link, { LinkProps } from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/layout.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/layout.tsx new file mode 100644 index 00000000000000..54b2b3630b4321 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/layout.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link' +import { ReactNode } from 'react' + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + +
    + {children} + + + ) +} + +function Header() { + return ( +
    + + Home + +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/page.tsx new file mode 100644 index 00000000000000..3d08470aa081f5 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/page.tsx @@ -0,0 +1,58 @@ +import { DebugLinkAccordion } from '../components/link-accordion' +import { unstable_cacheLife } from 'next/cache' + +export default async function Page() { + 'use cache' + unstable_cacheLife('minutes') + return ( +
    +

    shared layout prefetching - layout with cookies and dynamic data

    +
      +
    • + +
    • +
    • + +
    • +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    shared layout prefetching - layout with cookies

    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/layout.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/layout.tsx new file mode 100644 index 00000000000000..e6723448ba9b44 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/layout.tsx @@ -0,0 +1,34 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../shared' + +export default async function Layout({ children }) { + return ( +
    +
    +

    Shared layout

    + +

    + This shared layout uses cookies and no uncached IO, so it should be + completely runtime-prefetchable. +

    + Loading 1...
    }> + + + +
    + {children} +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/one/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/one/page.tsx new file mode 100644 index 00000000000000..85066bc245ac47 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/one/page.tsx @@ -0,0 +1,34 @@ +import { Suspense } from 'react' +import { cachedDelay } from '../../shared' +import { cookies } from 'next/headers' + +export default function Page() { + return ( +
    +

    Page one

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + + {/* + TODO: a runtime-prefetched layout that had no holes itself will still be considered partial + if any other segment in the response is partial, because we don't track partiality per-segment, + so if we want to test that full prefetches can reuse layouts from runtime prefetches, + the whole page needs to be dynamically prerenderable. + */} + {/* Loading 2...
    }> + + */} + + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/two/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/two/page.tsx new file mode 100644 index 00000000000000..604205bb78bba6 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/runtime-prefetchable-layout/two/page.tsx @@ -0,0 +1,37 @@ +import { Suspense } from 'react' +import { cachedDelay, uncachedIO } from '../../shared' +import { cookies } from 'next/headers' + +export default function Page() { + return ( +
    +

    Page two

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + return ( +
    +
    Dynamic content from page two
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/layout.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/layout.tsx new file mode 100644 index 00000000000000..95197f5bc83752 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/layout.tsx @@ -0,0 +1,48 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../shared' +import { connection } from 'next/server' + +export default async function Layout({ children }) { + return ( +
    +
    +

    Shared layout

    + +

    + This shared layout uses cookies and some uncached IO, so parts of it + should be runtime-prefetchable. +

    + Loading 1...
    }> + + + +
    + {children} +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content from layout
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/one/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/one/page.tsx new file mode 100644 index 00000000000000..feecdec327dcec --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/one/page.tsx @@ -0,0 +1,37 @@ +import { Suspense } from 'react' +import { cachedDelay, uncachedIO } from '../../shared' +import { cookies } from 'next/headers' + +export default function Page() { + return ( +
    +

    Page one

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + return ( +
    +
    Dynamic content from page one
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/two/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/two/page.tsx new file mode 100644 index 00000000000000..604205bb78bba6 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared-layout/two/page.tsx @@ -0,0 +1,37 @@ +import { Suspense } from 'react' +import { cachedDelay, uncachedIO } from '../../shared' +import { cookies } from 'next/headers' + +export default function Page() { + return ( +
    +

    Page two

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + return ( +
    +
    Dynamic content from page two
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared.tsx new file mode 100644 index 00000000000000..4bdbb6e93655a5 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/app/shared.tsx @@ -0,0 +1,34 @@ +import { unstable_cacheLife } from 'next/cache' +import { setTimeout } from 'timers/promises' + +export async function uncachedIO() { + await setTimeout(500) +} + +export async function cachedDelay(time: number, cacheBuster?: any) { + 'use cache' + unstable_cacheLife('minutes') + console.log('cachedDelay', time, cacheBuster) + await setTimeout(time) +} + +export function DebugRenderKind() { + const { workUnitAsyncStorage } = + require('next/dist/server/app-render/work-unit-async-storage.external') as typeof import('next/dist/server/app-render/work-unit-async-storage.external') + const workUnitStore = workUnitAsyncStorage.getStore()! + return ( +
    + workUnitStore.type: {workUnitStore.type} + {(() => { + switch (workUnitStore.type) { + case 'prerender': + return '(static prefetch)' + case 'prerender-runtime': + return '(runtime prefetch)' + default: + return null + } + })()} +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/components/link-accordion.tsx new file mode 100644 index 00000000000000..19498939be0664 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/components/link-accordion.tsx @@ -0,0 +1,63 @@ +'use client' + +import Link, { type LinkProps } from 'next/link' +import { ComponentProps, useState } from 'react' + +export function LinkAccordion({ + href, + children, + prefetch, +}: { + href: string + children: React.ReactNode + prefetch?: LinkProps['prefetch'] +}) { + const [isVisible, setIsVisible] = useState(false) + return ( + <> + setIsVisible(!isVisible)} + data-link-accordion={href} + data-prefetch={getPrefetchKind(prefetch)} + /> + {isVisible ? ( + + {children} + + ) : ( + <>{children} (link is hidden) + )} + + ) +} + +export function DebugLinkAccordion({ + href, + prefetch, +}: Omit, 'children'>) { + const prefetchKind = getPrefetchKind(prefetch) + return ( + + {href} ({prefetchKind}) + + ) +} + +function getPrefetchKind(prefetch: LinkProps['prefetch']) { + switch (prefetch) { + case false: + return 'disabled' + case undefined: + case null: + case 'auto': + return 'auto' + case true: + return 'runtime' + case 'unstable_forceStale': + return 'full' + default: + prefetch satisfies never + } +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/next.config.ts b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/next.config.ts new file mode 100644 index 00000000000000..728848f57dc2d3 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + experimental: { cacheComponents: true, clientSegmentCache: true }, + productionBrowserSourceMaps: true, +} + +export default nextConfig diff --git a/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts new file mode 100644 index 00000000000000..1556d39ea290b0 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-layout-sharing/prefetch-layout-sharing.test.ts @@ -0,0 +1,575 @@ +import { nextTestSetup } from 'e2e-utils' +import type * as Playwright from 'playwright' +import { createRouterAct } from '../router-act' + +describe('layout sharing in non-static prefetches', () => { + const { next, isNextDev } = nextTestSetup({ + files: __dirname, + }) + if (isNextDev) { + it('disabled in development', () => {}) + return + } + + // Glossary: + // + // - A "full prefetch" is `` (a.k.a old `prefetch={true}`, before cacheComponents). + // It includes cached and uncached IO. + + // - A "runtime prefetch" is the new `` (only available in cacheComponents mode). + // It includes cached IO, and allows access to cookies/params/searchParams/"use cache: private", but excludes uncached IO. + + it('runtime prefetches should omit layouts that were already prefetched with a runtime prefetch', async () => { + // Prefetches should re-use results from previous prefetches with the same fetch strategy. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a runtime prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/shared-layout/one"]` + ) + await linkToggle.click() + }, [ + // should prefetch page one, and allow reading cookies + { + includes: 'Cookie from page: testValue', + }, + // Should not prefetch any dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Reveal the link to trigger a runtime prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/shared-layout/two"]` + ) + await linkToggle.click() + }, [ + // Should not prefetch the shared layout, because we already have it in the cache + { + includes: 'Cookie from layout: testValue', + block: 'reject', + }, + // should prefetch page two, and allow reading cookies + { + includes: 'Cookie from page: testValue', + }, + // Should not prefetch the dynamic content from either of them + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Navigate to page two + await act(async () => { + await act( + async () => { + await browser.elementByCss(`a[href="/shared-layout/two"]`).click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + }) + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + expect(await browser.elementById('dynamic-content-layout').text()).toEqual( + 'Dynamic content from layout' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) + + it('full prefetches should omit layouts that were already prefetched with a full prefetch', async () => { + // Prefetches should re-use results from previous prefetches with the same fetch strategy. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a full prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="full"][data-link-accordion="/shared-layout/one"]` + ) + await linkToggle.click() + }, [ + // Should prefetch the dynamic content + { + includes: 'Dynamic content from page one', + }, + ]) + + // Reveal the link to trigger a full prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="full"][data-link-accordion="/shared-layout/two"]` + ) + await linkToggle.click() + }, [ + // Should not prefetch the shared layout, because we already have it in the cache + { + includes: 'Dynamic content from layout', + block: 'reject', + }, + // Should prefetch the dynamic content + { + includes: 'Dynamic content from page two', + }, + ]) + + // Navigate to page two. We have everything in the cache, so we shouldn't issue any new requests + await act(async () => { + await browser.elementByCss(`a[href="/shared-layout/two"]`).click() + }, 'no-requests') + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('dynamic-content-layout').text()).toEqual( + 'Dynamic content from layout' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) + + it('navigations should omit layouts that were already prefetched with a full prefetch', async () => { + // A navigation is mostly equivalent to a full prefetch, so it should re-use results from full prefetches. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a full prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="full"][data-link-accordion="/shared-layout/one"]` + ) + await linkToggle.click() + }, [ + // Should prefetch the dynamic content + { + includes: 'Dynamic content from page one', + }, + ]) + + // Reveal the link to trigger an auto prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="auto"][data-link-accordion="/shared-layout/two"]` + ) + await linkToggle.click() + }) + + // Navigate to page two. We have everything in the cache, so we shouldn't issue any new requests + await act(async () => { + await browser.elementByCss(`a[href="/shared-layout/two"]`).click() + }, [ + // Should not fetch the shared layout, because we already have it in the cache + { + includes: 'Dynamic content from layout', + block: 'reject', + }, + // Should fetch the dynamic content + { + includes: 'Dynamic content from page two', + }, + ]) + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('dynamic-content-layout').text()).toEqual( + 'Dynamic content from layout' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) + + it('runtime prefetches should omit layouts that were already prefetched with a full prefetch', async () => { + // A prefetch should re-use layouts from past prefetches with more specific fetch strategies. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a full prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="full"][data-link-accordion="/shared-layout/one"]` + ) + await linkToggle.click() + }, [ + // Should prefetch the dynamic content + { + includes: 'Dynamic content from page one', + }, + ]) + + // Reveal the link to trigger a runtime prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/shared-layout/two"]` + ) + await linkToggle.click() + }, [ + // Should not prefetch the shared layout, because we already have it in the cache + { + includes: 'Cookie from layout: testValue', + block: 'reject', + }, + // should prefetch page two, and allow reading cookies + { + includes: 'Cookie from page: testValue', + }, + // Should not prefetch any dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Navigate to page two + await act(async () => { + await act(async () => { + await browser.elementByCss(`a[href="/shared-layout/two"]`).click() + }, [ + // Should not fetch the shared layout, because we already have a full prefetch of it + { + includes: 'Cookie from layout: testValue', + block: 'reject', + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + }, + ]) + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + }) + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + expect(await browser.elementById('dynamic-content-layout').text()).toEqual( + 'Dynamic content from layout' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) + + it('full prefetches should include layouts that were only prefetched with a runtime prefetch', async () => { + // A prefetch should NOT re-use layouts from past prefetches if they used a less specific fetch strategy. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a runtime prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/shared-layout/one"]` + ) + await linkToggle.click() + }, [ + // should prefetch page one, and allow reading cookies + { + includes: 'Cookie from page: testValue', + }, + // Should not prefetch any dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Reveal the link to trigger a full prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="full"][data-link-accordion="/shared-layout/two"]` + ) + await linkToggle.click() + }, [ + // Should prefetch the shared layout, because we didn't prefetch it fully + { + includes: 'Dynamic content from layout', + }, + ]) + + // Navigate to page two. We have everything in the cache, so we shouldn't issue any new requests + await act(async () => { + await browser.elementByCss(`a[href="/shared-layout/two"]`).click() + }, 'no-requests') + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + expect(await browser.elementById('dynamic-content-layout').text()).toEqual( + 'Dynamic content from layout' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) + + it('full prefetches should omit layouts that were prefetched with a runtime prefetch and had no dynamic holes', async () => { + // If a runtime prefetch gave us a complete segment with no dynamic holes left, then it's equivalent to a full prefetch. + // + // TODO: This doesn't work in all cases -- if any segment in a runtime prefetch was partial, we'll mark all of them as partial, + // which means they can't be reused in a full prefetch or a navigation. So this only works if the dynaimic prefetch has no holes at all. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a runtime prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/runtime-prefetchable-layout/one"]` + ) + await linkToggle.click() + }, [ + // should prefetch page one, and allow reading cookies + { + includes: 'Cookie from page: testValue', + }, + ]) + + // Navigate to page one. It should have been completely prefetched by the runtime prefetch. + await act(async () => { + await browser + .elementByCss(`a[href="/runtime-prefetchable-layout/one"]`) + .click() + }, 'no-requests') + + await browser.back() + + // Reveal the link to trigger a full prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="full"][data-link-accordion="/runtime-prefetchable-layout/two"]` + ) + await linkToggle.click() + }, [ + // Should not prefetch the shared layout, because we already got a complete result for it. + { + includes: 'Cookie from layout', + block: 'reject', + }, + // Should fully prefetch the page, which we haven't prefetched before. + { + includes: 'Dynamic content from page two', + }, + ]) + + // Navigate to page two. We have everything in the cache, so we shouldn't issue any new requests + await act(async () => { + await browser + .elementByCss(`a[href="/runtime-prefetchable-layout/two"]`) + .click() + }, 'no-requests') + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) + + it('navigations should omit layouts that were prefetched with a runtime prefetch and had no dynamic holes', async () => { + // If a runtime prefetch gave us a complete segment with no dynamic holes left, then it's equivalent to a full prefetch. + // + // TODO: This doesn't work in all cases -- if any segment in a runtime prefetch was partial, we'll mark all of them as partial, + // which means they can't be reused in a full prefetch or a navigation. So this only works if the dynaimic prefetch has no holes at all. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'testValue' }) + + // Reveal the link to trigger a runtime prefetch for page one + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/runtime-prefetchable-layout/one"]` + ) + await linkToggle.click() + }, [ + // should prefetch page one, and allow reading cookies + { + includes: 'Cookie from page: testValue', + }, + ]) + + // Navigate to page one. It should have been completely prefetched by the runtime prefetch. + await act(async () => { + await browser + .elementByCss(`a[href="/runtime-prefetchable-layout/one"]`) + .click() + }, 'no-requests') + + await browser.back() + + // Reveal the link to trigger an auto prefetch for page two + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="auto"][data-link-accordion="/runtime-prefetchable-layout/two"]` + ) + await linkToggle.click() + }) + + // Navigate to page two. We need to request the page segment dynamically, but the shared layout should be cached. + await act(async () => { + await browser + .elementByCss(`a[href="/runtime-prefetchable-layout/two"]`) + .click() + }, [ + // Should not fetch the shared layout, because we already got a complete result for it. + { + includes: 'Cookie from layout', + block: 'reject', + }, + // Should fetch the page, which we haven't prefetched before. + { + includes: 'Dynamic content from page two', + }, + ]) + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementByCss('h2').text()).toEqual('Shared layout') + expect(await browser.elementByCss('h1').text()).toEqual('Page two') + expect(await browser.elementById('cookie-value-layout').text()).toEqual( + 'Cookie from layout: testValue' + ) + expect(await browser.elementById('cookie-value-page').text()).toEqual( + 'Cookie from page: testValue' + ) + expect(await browser.elementById('dynamic-content-page').text()).toEqual( + 'Dynamic content from page two' + ) + }) +}) + +function defer(callback: () => Promise) { + return { + [Symbol.asyncDispose]: callback, + } +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/private-short-stale/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/private-short-stale/page.tsx new file mode 100644 index 00000000000000..450b40e00671a7 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/private-short-stale/page.tsx @@ -0,0 +1,36 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' +import { unstable_cacheLife } from 'next/cache' + +export default async function Page() { + return ( +
    + +

    + This page uses a short-lived private cache (staleTime < + RUNTIME_PREFETCH_DYNAMIC_STALE, which is 30s), which should not be + included in a runtime prefetch +

    + Loading...}> + + +
    + ) +} + +async function CachedButShortLived() { + 'use cache: private' + unstable_cacheLife({ + stale: 5, + // the rest of the settings don't matter for private caches, + // because they are not persisted server-side + }) + await cachedDelay(500, [__filename]) + + return ( +
    + Short-lived cached content +
    {Date.now()}
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/public-short-expire-long-stale/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/public-short-expire-long-stale/page.tsx new file mode 100644 index 00000000000000..6e73655bc322fd --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/public-short-expire-long-stale/page.tsx @@ -0,0 +1,37 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' +import { unstable_cacheLife } from 'next/cache' + +export default async function Page() { + return ( +
    + +

    + This page uses a short-lived public cache (expire < DYNAMIC_EXPIRE, + 5min), which should not be included in a static prefetch, but should be + included in a runtime prefetch, because it has a long enough stale time + (> RUNTIME_PREFETCH_DYNAMIC_STALE, 30s) +

    + Loading...}> + + +
    + ) +} + +async function ShortLivedCache() { + 'use cache' + unstable_cacheLife({ + stale: 60, // > RUNTIME_PREFETCH_DYNAMIC_STALE + revalidate: 2 * 60, + expire: 3 * 60, // < DYNAMIC_EXPIRE + }) + await cachedDelay(500, [__filename]) + + return ( +
    + Short-lived cached content +
    {Date.now()}
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/public-short-expire-short-stale/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/public-short-expire-short-stale/page.tsx new file mode 100644 index 00000000000000..1d077484086bba --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/caches/public-short-expire-short-stale/page.tsx @@ -0,0 +1,37 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' +import { unstable_cacheLife } from 'next/cache' + +export default async function Page() { + return ( +
    + +

    + This page uses a short-lived public cache (expire < DYNAMIC_EXPIRE, + 5min), which should not be included in a static prefetch, and should + also not be included in a runtime prefetch, because it has a short + enough stale time (< RUNTIME_PREFETCH_DYNAMIC_STALE, 30s) +

    + Loading...}> + + +
    + ) +} + +async function ShortLivedCache() { + 'use cache' + unstable_cacheLife({ + stale: 20, // < RUNTIME_PREFETCH_DYNAMIC_STALE + revalidate: 2 * 60, + expire: 3 * 60, // < DYNAMIC_EXPIRE + }) + await cachedDelay(500, [__filename]) + + return ( +
    + Short-lived cached content +
    {Date.now()}
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/error-after-cookies/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/error-after-cookies/page.tsx new file mode 100644 index 00000000000000..d844a29339a6f0 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/error-after-cookies/page.tsx @@ -0,0 +1,28 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' +import { ErrorBoundary } from '../../../../components/error-boundary' + +export default async function Page() { + return ( +
    + +

    + This page errors after a cookies call, so we should only see the error + in a runtime prefetch or a navigation (and not during prerendering / + prefetching) +

    + Loading 1...}> + + + + +
    + ) +} + +async function One(): Promise { + const cookieStore = await cookies() + await cachedDelay(500, ['/cookies', cookieStore.get('user-agent')?.value]) + throw new Error('Kaboom') +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/sync-io-after-cookies/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/sync-io-after-cookies/page.tsx new file mode 100644 index 00000000000000..d7b1d65fa23a1b --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/errors/sync-io-after-cookies/page.tsx @@ -0,0 +1,25 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' + +export default async function Page() { + return ( +
    + +

    + This page performs sync IO after a cookies() call, so we should only see + the error in a runtime prefetch or a navigation (and not during + prerendering / prefetching) +

    + Loading 1...}> + + +
    + ) +} + +async function One() { + const cookieStore = await cookies() + await cachedDelay(500, ['/cookies', cookieStore.get('user-agent')?.value]) + return
    Timestamp: {Date.now()}
    +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/fully-static/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/fully-static/page.tsx new file mode 100644 index 00000000000000..0a162d0ab1ae9c --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/fully-static/page.tsx @@ -0,0 +1,13 @@ +export default async function Page() { + return ( +
    +

    Fully static

    +

    Hello from a fully static page!

    +

    + {new Array({ length: 1000 }) + .fill(null) + .map(() => 'Lorem ipsum dolor sit amet.')} +

    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies-only/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies-only/page.tsx new file mode 100644 index 00000000000000..d9795d0c6d4008 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies-only/page.tsx @@ -0,0 +1,29 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' + +export default async function Page() { + return ( +
    + +

    + This page uses cookies and no uncached IO, So it should be completely + prefetchable with a runtime prefetch. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies/page.tsx new file mode 100644 index 00000000000000..eda5cfa098249e --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/cookies/page.tsx @@ -0,0 +1,56 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared' +import { connection } from 'next/server' + +export default async function Page() { + return ( +
    + +

    + This page uses cookies and some uncached IO, so parts of it should be + runtime-prefetchable. +

    + Loading 1...}> + + +
    { + 'use server' + const cookieStore = await cookies() + const cookieValue = formData.get('cookie') as string | null + if (cookieValue) { + cookieStore.set('testCookie', cookieValue) + } + }} + > + + +
    +
    + ) +} + +async function RuntimePrefetchable() { + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return ( +
    + + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/dynamic-params/[id]/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/dynamic-params/[id]/page.tsx new file mode 100644 index 00000000000000..e046e9892944e3 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/dynamic-params/[id]/page.tsx @@ -0,0 +1,43 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared' +import { connection } from 'next/server' + +type Params = { id: string } + +export default async function Page({ params }: { params: Promise }) { + return ( +
    + +

    + This page uses params and some uncached IO, so parts of it should be + runtime-prefetchable. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable({ params }: { params: Promise }) { + const { id } = await params + await cachedDelay(500, [__filename, id]) + return ( +
    +
    {`Param: ${id}`}
    + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/search-params/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/search-params/page.tsx new file mode 100644 index 00000000000000..4d79c239a56c42 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-page/search-params/page.tsx @@ -0,0 +1,51 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared' +import { connection } from 'next/server' + +type AnySearchParams = { [key: string]: string | string[] | undefined } + +export default async function Page({ + searchParams, +}: { + searchParams: Promise +}) { + return ( +
    + +

    + This page uses search params and some uncached IO, so parts of it should + be runtime-prefetchable. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable({ + searchParams, +}: { + searchParams: Promise +}) { + const { searchParam } = await searchParams + await cachedDelay(500, [__filename, searchParam]) + return ( +
    +
    {`Search param: ${searchParam}`}
    + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/cookies-only/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/cookies-only/page.tsx new file mode 100644 index 00000000000000..d8e157d6c7c437 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/cookies-only/page.tsx @@ -0,0 +1,35 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind } from '../../../shared' + +export default async function Page() { + return ( +
    + +

    + This page uses cookies (from a private cache) and no uncached IO, So it + should be completely prefetchable with a runtime prefetch. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const cookieValue = await privateCache() + return ( +
    + +
    + ) +} + +async function privateCache() { + 'use cache: private' + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return cookieValue +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/cookies/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/cookies/page.tsx new file mode 100644 index 00000000000000..af346dd3c10e23 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/cookies/page.tsx @@ -0,0 +1,62 @@ +import { cookies } from 'next/headers' +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared' +import { connection } from 'next/server' + +export default async function Page() { + return ( +
    + +

    + This page uses cookies (from inside a private cache) and some uncached + IO, so parts of it should be runtime-prefetchable. +

    + Loading 1...}> + + +
    { + 'use server' + const cookieStore = await cookies() + const cookieValue = formData.get('cookie') as string | null + if (cookieValue) { + cookieStore.set('testCookie', cookieValue) + } + }} + > + + +
    +
    + ) +} + +async function RuntimePrefetchable() { + const cookieValue = await privateCache() + return ( +
    + + Loading 2...
    }> + + + + ) +} + +async function privateCache() { + 'use cache: private' + const cookieStore = await cookies() + const cookieValue = cookieStore.get('testCookie')?.value ?? null + await cachedDelay(500, [__filename, cookieValue]) + return cookieValue +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/date-now/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/date-now/page.tsx new file mode 100644 index 00000000000000..6aafaeb34f14e6 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/date-now/page.tsx @@ -0,0 +1,47 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared' +import { connection } from 'next/server' + +export default async function Page() { + return ( +
    + +

    + This page uses Date.now (in a private cache) and some uncached IO, so + parts of it should be runtime-prefetchable. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable() { + const now = await privateCache() + return ( +
    +
    {`Timestamp: ${now}`}
    + Loading 2...
    }> + + + + ) +} + +async function privateCache() { + 'use cache: private' + const now = Date.now() + await cachedDelay(500, [__filename]) + return now +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/dynamic-params/[id]/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/dynamic-params/[id]/page.tsx new file mode 100644 index 00000000000000..c6a670d069c64e --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/dynamic-params/[id]/page.tsx @@ -0,0 +1,49 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared' +import { connection } from 'next/server' + +type Params = { id: string } + +export default async function Page({ params }: { params: Promise }) { + return ( +
    + +

    + This page uses params (passed to a private cache) and some uncached IO, + so parts of it should be runtime-prefetchable. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable({ params }: { params: Promise }) { + const id = await privateCache(params) + return ( +
    +
    {`Param: ${id}`}
    + Loading 2...
    }> + + + + ) +} + +async function privateCache(params: Promise) { + 'use cache: private' + const { id } = await params + await cachedDelay(500, [__filename, id]) + return id +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/search-params/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/search-params/page.tsx new file mode 100644 index 00000000000000..9b5e7a08db6614 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/in-private-cache/search-params/page.tsx @@ -0,0 +1,57 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../shared' +import { connection } from 'next/server' + +type AnySearchParams = { [key: string]: string | string[] | undefined } + +export default async function Page({ + searchParams, +}: { + searchParams: Promise +}) { + return ( +
    + +

    + This page uses search params (passed to a private cache) and some + uncached IO, so parts of it should be runtime-prefetchable. +

    + Loading 1...}> + + +
    + ) +} + +async function RuntimePrefetchable({ + searchParams, +}: { + searchParams: Promise +}) { + const searchParam = await privateCache(searchParams) + return ( +
    +
    {`Search param: ${searchParam}`}
    + Loading 2...
    }> + + + + ) +} + +async function privateCache(searchParams: Promise) { + 'use cache: private' + const { searchParam } = await searchParams + await cachedDelay(500, [__filename, searchParam]) + return searchParam +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/layout.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/layout.tsx new file mode 100644 index 00000000000000..54b2b3630b4321 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/layout.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link' +import { ReactNode } from 'react' + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + +
    + {children} + + + ) +} + +function Header() { + return ( +
    + + Home + +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx new file mode 100644 index 00000000000000..3fb48a5db586bc --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/(default)/page.tsx @@ -0,0 +1,218 @@ +import { DebugLinkAccordion } from '../../components/link-accordion' + +export default async function Page() { + return ( +
    +

    Home

    + +

    directly in a page

    +
      +
    • + cookies + dynamic content +
        +
      • + +
      • +
      +
    • + +
    • + search params + dynamic content +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    • + dynamic params + dynamic content +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    • + only cookies +
        +
      • + +
      • +
      +
    • +
    + +

    + use cache: private +

    +
      +
    • + cookies in private cache + dynamic content +
        +
      • + +
      • +
      +
    • +
    • + dynamic params in private cache + dynamic content +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    • + search params in private cache + dynamic content +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    • + only cookies in private cache +
        +
      • + +
      • +
      +
    • +
    • + Date.now() in private cache +
        +
      • + +
      • +
      +
    • +
    + +

    short-lived caches

    +
      +
    • + private, short stale +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    • + public, short expire, long enough stale +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    • + public, short expire, short stale +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    + +

    misc

    +
      +
    • + +
    • +
    + +

    errors

    +
      +
    • + +
    • +
    • + +
    • +
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/shared.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/shared.tsx new file mode 100644 index 00000000000000..4bdbb6e93655a5 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/shared.tsx @@ -0,0 +1,34 @@ +import { unstable_cacheLife } from 'next/cache' +import { setTimeout } from 'timers/promises' + +export async function uncachedIO() { + await setTimeout(500) +} + +export async function cachedDelay(time: number, cacheBuster?: any) { + 'use cache' + unstable_cacheLife('minutes') + console.log('cachedDelay', time, cacheBuster) + await setTimeout(time) +} + +export function DebugRenderKind() { + const { workUnitAsyncStorage } = + require('next/dist/server/app-render/work-unit-async-storage.external') as typeof import('next/dist/server/app-render/work-unit-async-storage.external') + const workUnitStore = workUnitAsyncStorage.getStore()! + return ( +
    + workUnitStore.type: {workUnitStore.type} + {(() => { + switch (workUnitStore.type) { + case 'prerender': + return '(static prefetch)' + case 'prerender-runtime': + return '(runtime prefetch)' + default: + return null + } + })()} +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/in-page/root-params/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/in-page/root-params/page.tsx new file mode 100644 index 00000000000000..287fa05d34e91f --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/in-page/root-params/page.tsx @@ -0,0 +1,41 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared' +import { connection } from 'next/server' +import { lang } from 'next/root-params' + +export default async function Page() { + return ( +
    + +

    + This page uses root params and some uncached IO. Root params should + always be available in static prerenders, so a runtime prefetch should + have them too. +

    + +
    + ) +} + +async function StaticallyPrefetchable() { + const currentLang = await lang() + await cachedDelay(500, [__filename, currentLang]) + return ( +
    +
    {`Lang: ${currentLang}`}
    + Loading 2...
    }> + + + + ) +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/in-private-cache/root-params/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/in-private-cache/root-params/page.tsx new file mode 100644 index 00000000000000..8567c41156f852 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/in-private-cache/root-params/page.tsx @@ -0,0 +1,50 @@ +import { Suspense } from 'react' +import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared' +import { connection } from 'next/server' +import { lang } from 'next/root-params' + +export default async function Page() { + return ( +
    + +

    + This page uses root params (inside a private cache) and some uncached + IO. Private caches are only rendered during runtime prefetches and + navigation requests, so they won't be part of a static prefetch, but + they should be part of a runtime prefetch. +

    + + + +
    + ) +} + +async function DynamicallyPrefetchable() { + const currentLang = await privateCache() + return ( +
    +
    {`Lang: ${currentLang}`}
    + Loading 2...
    }> + + + + ) +} + +async function privateCache() { + 'use cache: private' + const currentLang = await lang() + await cachedDelay(500, [__filename, currentLang]) + return currentLang +} + +async function Dynamic() { + await uncachedIO() + await connection() + return ( +
    +
    Dynamic content
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/layout.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/layout.tsx new file mode 100644 index 00000000000000..3042ae2c18006c --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/layout.tsx @@ -0,0 +1,34 @@ +import Link from 'next/link' +import { lang } from 'next/root-params' +import { ReactNode } from 'react' + +export default async function RootLayout({ + children, +}: { + children: ReactNode +}) { + const currentLang = await lang() + return ( + + +
    + {children} + + + ) +} + +async function Header() { + const currentLang = await lang() + return ( +
    + + Home (for lang: {currentLang}) + +
    + ) +} + +export function generateStaticParams() { + return [{ lang: 'en' }] +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/page.tsx new file mode 100644 index 00000000000000..97cbf8adec0174 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/app/with-root-param/[lang]/page.tsx @@ -0,0 +1,56 @@ +import { lang } from 'next/root-params' +import { DebugLinkAccordion } from '../../../components/link-accordion' + +export default async function Page() { + const currentLang = await lang() + const otherLang = currentLang === 'en' ? 'de' : 'en' + return ( +
    +

    Home - with root param ({currentLang})

    + +

    directly in a page

    +
      +
    • + root params + dynamic content +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    + +

    + use cache: private +

    +
      +
    • + root params + dynamic content +
        +
      • + +
      • +
      • + +
      • +
      +
    • +
    +
    + ) +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/components/error-boundary.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/components/error-boundary.tsx new file mode 100644 index 00000000000000..64fb109773e01e --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/components/error-boundary.tsx @@ -0,0 +1,24 @@ +'use client' + +import React from 'react' + +export class ErrorBoundary extends React.Component<{ + children: React.ReactNode +}> { + state = { error: null } + + static getDerivedStateFromError(error) { + return { error } + } + + render() { + if (this.state.error) { + return ( +
    + Error boundary: {this.state.error.message} +
    + ) + } + return this.props.children + } +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/prefetch-runtime/components/link-accordion.tsx new file mode 100644 index 00000000000000..19498939be0664 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/components/link-accordion.tsx @@ -0,0 +1,63 @@ +'use client' + +import Link, { type LinkProps } from 'next/link' +import { ComponentProps, useState } from 'react' + +export function LinkAccordion({ + href, + children, + prefetch, +}: { + href: string + children: React.ReactNode + prefetch?: LinkProps['prefetch'] +}) { + const [isVisible, setIsVisible] = useState(false) + return ( + <> + setIsVisible(!isVisible)} + data-link-accordion={href} + data-prefetch={getPrefetchKind(prefetch)} + /> + {isVisible ? ( + + {children} + + ) : ( + <>{children} (link is hidden) + )} + + ) +} + +export function DebugLinkAccordion({ + href, + prefetch, +}: Omit, 'children'>) { + const prefetchKind = getPrefetchKind(prefetch) + return ( + + {href} ({prefetchKind}) + + ) +} + +function getPrefetchKind(prefetch: LinkProps['prefetch']) { + switch (prefetch) { + case false: + return 'disabled' + case undefined: + case null: + case 'auto': + return 'auto' + case true: + return 'runtime' + case 'unstable_forceStale': + return 'full' + default: + prefetch satisfies never + } +} diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/next.config.ts b/test/e2e/app-dir/segment-cache/prefetch-runtime/next.config.ts new file mode 100644 index 00000000000000..728848f57dc2d3 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + experimental: { cacheComponents: true, clientSegmentCache: true }, + productionBrowserSourceMaps: true, +} + +export default nextConfig diff --git a/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts b/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts new file mode 100644 index 00000000000000..3881ed0f83db1c --- /dev/null +++ b/test/e2e/app-dir/segment-cache/prefetch-runtime/prefetch-runtime.test.ts @@ -0,0 +1,1061 @@ +import { nextTestSetup } from 'e2e-utils' +import type * as Playwright from 'playwright' +import { createRouterAct } from '../router-act' + +describe(' (runtime prefetch)', () => { + const { next, isNextDev, isNextDeploy } = nextTestSetup({ + files: __dirname, + }) + if (isNextDev) { + it('disabled in development', () => {}) + return + } + + describe.each([ + { description: 'in a page', prefix: 'in-page' }, + { description: 'in a private cache', prefix: 'in-private-cache' }, + ])('$description', ({ prefix }) => { + it('includes dynamic params, but not dynamic content', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + // Reveal the link to trigger a runtime prefetch for one value of the dynamic param + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/dynamic-params/123"]` + ) + await linkToggle.click() + }, [ + // Should allow reading dynamic params + { + includes: 'Param: 123', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Reveal the link to trigger a runtime prefetch for a different value of the dynamic param + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/dynamic-params/456"]` + ) + await linkToggle.click() + }, [ + // Should allow reading dynamic params + { + includes: 'Param: 456', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Navigate to the page + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/${prefix}/dynamic-params/123"]`) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('param-value').text()).toEqual( + 'Param: 123' + ) + }) + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('param-value').text()).toEqual( + 'Param: 123' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + + await browser.back() + + // Reveal the link to the second page again. It should not be prefetched again + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/dynamic-params/456"]` + ) + await linkToggle.click() + }, 'no-requests') + + // Navigate to the other page + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/${prefix}/dynamic-params/456"]`) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('param-value').text()).toEqual( + 'Param: 456' + ) + }) + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('param-value').text()).toEqual( + 'Param: 456' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + }) + + it('includes root params, but not dynamic content', async () => { + let page: Playwright.Page + const browser = await next.browser('/with-root-param/en', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + // Reveal the link to trigger a runtime prefetch for one value of the root param + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/with-root-param/en/${prefix}/root-params"]` + ) + await linkToggle.click() + }, [ + // Should allow reading root params + { + includes: 'Lang: en', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // TODO(runtime-ppr) - visiting root params that weren't in generateStaticParams errors when deployed + if (!isNextDeploy) { + // Reveal the link to trigger a runtime prefetch for a different value of the root param + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/with-root-param/de/${prefix}/root-params"]` + ) + await linkToggle.click() + }, [ + // Should allow reading root params + { + includes: 'Lang: de', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + } + + // Navigate to the first page + await act(async () => { + await act( + async () => { + await browser + .elementByCss( + `a[href="/with-root-param/en/${prefix}/root-params"]` + ) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('root-param-value').text()).toEqual( + 'Lang: en' + ) + }) + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('root-param-value').text()).toEqual( + 'Lang: en' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + + // TODO(runtime-ppr) - visiting root params that weren't in generateStaticParams errors when deployed + if (!isNextDeploy) { + await browser.back() + + // Reveal the link to the second page again. It should not be prefetched again + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/with-root-param/de/${prefix}/root-params"]` + ) + await linkToggle.click() + }, 'no-requests') + + // Navigate to the other page + await act(async () => { + await act( + async () => { + await browser + .elementByCss( + `a[href="/with-root-param/de/${prefix}/root-params"]` + ) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('root-param-value').text()).toEqual( + 'Lang: de' + ) + }) + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('root-param-value').text()).toEqual( + 'Lang: de' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + } + }) + + it('includes search params, but not dynamic content', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + // Reveal the link to trigger a runtime prefetch for one value of the search param + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/search-params?searchParam=123"]` + ) + await linkToggle.click() + }, [ + // Should allow reading search params + { + includes: 'Search param: 123', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Reveal the link to trigger a runtime prefetch for a different value of the search param + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/search-params?searchParam=456"]` + ) + await linkToggle.click() + }, [ + // Should allow reading search params + { + includes: 'Search param: 456', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Navigate to the page + await act(async () => { + await act( + async () => { + await browser + .elementByCss( + `a[href="/${prefix}/search-params?searchParam=123"]` + ) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('search-param-value').text()).toEqual( + 'Search param: 123' + ) + }) + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('search-param-value').text()).toEqual( + 'Search param: 123' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + + await browser.back() + + // Reveal the link to the second page again. It should not be prefetched again + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/search-params?searchParam=456"]` + ) + await linkToggle.click() + }, 'no-requests') + + // Navigate to the other page + await act( + async () => { + await browser + .elementByCss(`a[href="/${prefix}/search-params?searchParam=456"]`) + .click() + }, + { + // Now the dynamic content should be fetched + includes: 'Dynamic content', + } + ) + expect(await browser.elementById('search-param-value').text()).toEqual( + 'Search param: 456' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + }) + + it('includes cookies, but not dynamic content', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'initialValue' }) + + // Reveal the link to trigger a runtime prefetch for the initial cookie value + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/cookies"]` + ) + await linkToggle.click() + }, [ + // Should allow reading cookies + { + includes: 'Cookie: initialValue', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Navigate to the page + await act(async () => { + await act( + async () => { + await browser.elementByCss(`a[href="/${prefix}/cookies"]`).click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('cookie-value').text()).toEqual( + 'Cookie: initialValue' + ) + }) + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('cookie-value').text()).toEqual( + 'Cookie: initialValue' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + + // Update the cookie via a server action. + // This should cause the client cache to be dropped, + // so the page should get prefetched again when the link becomes visible + await browser.elementByCss('input[name="cookie"]').type('updatedValue') + await browser.elementByCss('[type="submit"]').click() + + // Go back to the previous page + await browser.back() + + // Reveal the link again to trigger a runtime prefetch for the new value of the cookie + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/cookies"]` + ) + await linkToggle.click() + }, [ + // Should allow reading dynamic params + { + includes: 'Cookie: updatedValue', + }, + // Should not prefetch the dynamic content + { + includes: 'Dynamic content', + block: 'reject', + }, + ]) + + // Navigate to the page + await act(async () => { + await act( + async () => { + await browser.elementByCss(`a[href="/${prefix}/cookies"]`).click() + }, + { + includes: 'Dynamic content', + block: true, + } + ) + expect(await browser.elementById('cookie-value').text()).toEqual( + 'Cookie: updatedValue' + ) + }) + + expect(await browser.elementById('cookie-value').text()).toEqual( + 'Cookie: updatedValue' + ) + expect(await browser.elementById('dynamic-content').text()).toEqual( + 'Dynamic content' + ) + }) + + it('can completely prefetch a page that uses cookies and no uncached IO', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'initialValue' }) + + // Reveal the link to trigger a runtime prefetch for the initial cookie value + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/cookies-only"]` + ) + await linkToggle.click() + }, [ + // Should allow reading cookies + { + includes: 'Cookie: initialValue', + }, + ]) + + // Navigate to the page. + await act( + async () => { + await browser + .elementByCss(`a[href="/${prefix}/cookies-only"]`) + .click() + }, + // The page doesn't use any other IO, so we prefetched it completely, and shouldn't issue any more requests. + 'no-requests' + ) + expect(await browser.elementById('cookie-value').text()).toEqual( + 'Cookie: initialValue' + ) + }) + }) + + describe('should not cache runtime prefetch responses in the browser cache or server-side', () => { + // This is a bit difficult to test, but we can request the same thing repeatedly and expect different results. + + it.each([ + { description: 'in a page', prefix: 'in-page' }, + { description: 'in a private cache', prefix: 'in-private-cache' }, + ])( + 'different cookies should return different prefetch results - $description', + async ({ prefix }) => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + await browser.addCookie({ name: 'testCookie', value: 'initialValue' }) + + // Reveal the link to trigger a runtime prefetch for the initial cookie value + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/cookies-only"]` + ) + await linkToggle.click() + }, [ + // Should allow reading cookies + { + includes: 'Cookie: initialValue', + }, + ]) + + // Reload the page with a new cookie value + await browser.addCookie({ name: 'testCookie', value: 'updatedValue' }) + await browser.refresh() + + // Reveal the link to trigger a runtime prefetch for the updated cookie value. + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/${prefix}/cookies-only"]` + ) + await linkToggle.click() + }, [ + // The response shouldn't be cached in the browser or on the server. + // If it was, we'd get a stale value here. + { + includes: 'Cookie: updatedValue', + }, + ]) + } + ) + + it('private caches should return new results on each request', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + // Clear cookies after the test. This currently doesn't happen automatically. + await using _ = defer(() => browser.deleteCookies()) + + const act = createRouterAct(page) + + // Reveal the link to trigger the first runtime prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/in-private-cache/date-now"]` + ) + await linkToggle.click() + }, [ + // The timestamp value is in a private cache, so it should be included + { + includes: 'Timestamp: ', + }, + ]) + + // Navigate to the page to reveal the runtime-prefetched content, and save the timestamp value it had + let firstTimestampValue: string + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/in-private-cache/date-now"]`) + .click() + }, + // Temporarily block the navigation request. + // The prefetched parts of the tree should be visible before it finishes. + 'block' + ) + firstTimestampValue = await browser.elementById('timestamp').text() + }) + + // Go back to the initial page and reload it to clear the client router cache + await browser.back() + await browser.refresh() + + // Reveal the link to trigger the second runtime prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/in-private-cache/date-now"]` + ) + await linkToggle.click() + }, [ + // The timestamp value is in a private cache, so it should be included + { + includes: 'Timestamp: ', + }, + ]) + + // Navigate to the page to reveal the runtime-prefetched content, and save the timestamp value it had + let secondTimestampValue: string + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/in-private-cache/date-now"]`) + .click() + }, + // Temporarily block the navigation request. + // The prefetched parts of the tree should be visible before it finishes. + 'block' + ) + secondTimestampValue = await browser.elementById('timestamp').text() + }) + + // If the runtime prefetch response wasn't cached, the responses should be different + expect(firstTimestampValue).not.toEqual(secondTimestampValue) + }) + }) + + it('can completely prefetch a page that is fully static', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + + const act = createRouterAct(page) + + // Reveal the link to trigger a runtime prefetch for the page + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/fully-static"]` + ) + await linkToggle.click() + }, [ + { + includes: 'Hello from a fully static page!', + }, + ]) + + // Navigate to the page. + await act( + async () => { + await browser.elementByCss(`a[href="/fully-static"]`).click() + }, + // The page doesn't use any IO, so we prefetched it completely, and shouldn't issue any more requests. + 'no-requests' + ) + expect(await browser.elementByCss('p#intro').text()).toBe( + 'Hello from a fully static page!' + ) + }) + + describe('cache stale time handling', () => { + it('includes short-lived public caches with a long enough staleTime', async () => { + // If a cache has an expiration time under 5min (DYNAMIC_EXPIRE), we omit it from static prerenders. + // However, it should still be included in a runtime prefetch if it's stale time is above 30s. (RUNTIME_PREFETCH_DYNAMIC_STALE) + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + const STATIC_CONTENT = 'This page uses a short-lived public cache' + const DYNAMICALLY_PREFETCHABLE_CONTENT = 'Short-lived cached content' + + // Reveal the link to trigger a static prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="auto"][data-link-accordion="/caches/public-short-expire-long-stale"]` + ) + await linkToggle.click() + }, [ + // Should include the static shell + { + includes: STATIC_CONTENT, + }, + // Should not include the short-lived cache + // (We set the `expire` value to be under 5min, so it will be excluded from prerenders) + { + includes: DYNAMICALLY_PREFETCHABLE_CONTENT, + block: 'reject', + }, + ]) + + // Reveal the link to trigger a runtime prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/caches/public-short-expire-long-stale"]` + ) + await linkToggle.click() + }, [ + // Should include the short-lived cache + // (We set `stale` to be above 30s, which means it shouldn't be omitted) + { + includes: DYNAMICALLY_PREFETCHABLE_CONTENT, + }, + ]) + + // Navigate to the page. We didn't include any uncached IO, so the page is fully prefetched, + // and this shouldn't issue any more requests + await act(async () => { + await browser + .elementByCss(`a[href="/caches/public-short-expire-long-stale"]`) + .click() + }, 'no-requests') + + expect(await browser.elementByCss('main').text()).toInclude( + DYNAMICALLY_PREFETCHABLE_CONTENT + ) + }) + + it('omits short-lived public caches with a short enough staleTime', async () => { + // If a cache has a stale time below 30s (RUNTIME_PREFETCH_DYNAMIC_STALE), we should omit it from runtime prefetches. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + const STATIC_CONTENT = 'This page uses a short-lived public cache' + const DYNAMIC_CONTENT = 'Short-lived cached content' + + // Reveal the link to trigger a static prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="auto"][data-link-accordion="/caches/public-short-expire-short-stale"]` + ) + await linkToggle.click() + }, [ + // Should include the static shell + { + includes: STATIC_CONTENT, + }, + // Should not include the short-lived cache + // (We set the `expire` value to be under 5min, so it will be excluded from prerenders) + { + includes: DYNAMIC_CONTENT, + block: 'reject', + }, + ]) + + // Reveal the link to trigger a runtime prefetch. + // It'll essentially be the same as the static prefetch, because the only dynamic hole + // will be omitted from both. + // (NOTE: in the future, we might prevent scenarios like this via `generatePrefetch`) + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/caches/public-short-expire-short-stale"]` + ) + await linkToggle.click() + }, [ + // Should include the shell + { + includes: STATIC_CONTENT, + }, + // Should not include the short-lived cache + // (We set the `stale` value to be under 30s, so it will be excluded from runtime prerenders) + { + includes: DYNAMIC_CONTENT, + block: 'reject', + }, + ]) + + // Navigate to the page + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/caches/public-short-expire-short-stale"]`) + .click() + }, + { + // Temporarily block the navigation request. + // The prefetched parts of the tree should be visible before it finishes. + includes: DYNAMIC_CONTENT, + block: true, + } + ) + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + }) + + // After navigating, we should see both the parts that we prefetched and the short lived cache. + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + expect(await browser.elementById('cached-value').text()).toMatch(/\d+/) + }) + + it('omits private caches with a short enough staleTime', async () => { + // If a cache has a stale time below 30s (RUNTIME_PREFETCH_DYNAMIC_STALE), we should omit it from runtime prefetches. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + const STATIC_CONTENT = 'This page uses a short-lived private cache' + const DYNAMIC_CONTENT = 'Short-lived cached content' + + // Reveal the link to trigger a static prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="auto"][data-link-accordion="/caches/private-short-stale"]` + ) + await linkToggle.click() + }, [ + // Should include the static shell + { + includes: STATIC_CONTENT, + }, + // Should not include the short-lived cache + // (We set the `expire` value to be under 5min, so it will be excluded from prerenders) + { + includes: DYNAMIC_CONTENT, + block: 'reject', + }, + ]) + + // Reveal the link to trigger a runtime prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/caches/private-short-stale"]` + ) + await linkToggle.click() + }, [ + // Should include the shell + { + includes: STATIC_CONTENT, + }, + // Should not prefetch the short-lived cache + // (We set the `stale` value to be under 30s, so it will be excluded from runtime prefetches) + { + includes: DYNAMIC_CONTENT, + block: 'reject', + }, + ]) + + // Navigate to the page + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/caches/private-short-stale"]`) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: DYNAMIC_CONTENT, + block: true, + } + ) + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + }) + + // After navigating, we should see both the parts that we prefetched and dynamic content. + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + const cachedValue1 = await browser.elementById('cached-value').text() + expect(cachedValue1).toMatch(/\d+/) + + // Try navigating again. The cache is private, so we should see a different timestamp + await browser.back() + + // Reveal the link again. The prefetch should be cached, so we shouldn't see any requests + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-link-accordion="/caches/private-short-stale"]` + ) + await linkToggle.click() + }, 'no-requests') + + // Navigate to the page again + await act(async () => { + await act( + async () => { + await browser + .elementByCss(`a[href="/caches/private-short-stale"]`) + .click() + }, + { + // Temporarily block the navigation request. + // The runtime-prefetched parts of the tree should be visible before it finishes. + includes: 'Short-lived cached content', + block: true, + } + ) + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + }) + + // After navigating, we should see both the parts that we prefetched and dynamic content. + // The private cache was omitted from the runtime prefetch, so we didn't cache it in the router, + // and it was not cached server-side either, so we should get a different value than the previous request. + const cachedValue2 = await browser.elementById('cached-value').text() + expect(cachedValue2).toMatch(/\d+/) + + expect(cachedValue1).not.toEqual(cachedValue2) + }) + }) + + describe('errors', () => { + it('aborts the prerender and logs an error when sync IO is used after cookies()', async () => { + // In a runtime prefetch, we might encounter sync IO usages that weren't caught during build, + // because they were hidden behind e.g. a cookies() call. + // We currently have no way to catch these statically. + // In that case, we should abort the prerender, but still return partial content. + + // TODO: this doesn't work as well as it could, see comment before the navigation + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + const STATIC_CONTENT = 'This page performs sync IO after a cookies() call' + + // Reveal the link to trigger a runtime prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/errors/sync-io-after-cookies"]` + ) + await linkToggle.click() + }, [ + // Should include the shell + { + includes: STATIC_CONTENT, + }, + // Should abort the render when sync IO is encountered, + // so this should never be included + { + includes: 'Timestamp', + block: 'reject', + }, + ]) + + if (!isNextDeploy) { + expect(next.cliOutput).toContain( + 'Error: Route "/errors/sync-io-after-cookies" used `Date.now()` instead of using `performance` or without explicitly calling `await connection()` beforehand.' + ) + } + + // TODO(runtime-ppr): we should be able to display the (aborted, partial) prefetched contents before navigating, + // but it seems like when we abort the render, we're also inadvertently cutting off some promises related to metadata + // which end up suspending on the client and blocking react from rendering. + // Ideally, we'd be able to use the partial result, but this is an error scenario and we don't crash, + // so i'm just settling for that for now. + + // Navigate to the page + await act( + async () => { + await browser + .elementByCss(`a[href="/errors/sync-io-after-cookies"]`) + .click() + }, + { + includes: 'Timestamp', + } + ) + + // After navigating, we should see the sync IO result that we omitted from the prefetch. + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + expect(await browser.elementById('timestamp').text()).toMatch( + /Timestamp: \d+/ + ) + }) + + it('should trigger error boundaries for errors that occurred in runtime-prefetched content', async () => { + // A thrown error in the prerender should not stop us from sending a prefetch response. + // This should work without any extra effort, but I'm adding a test for it as a sanity check. + + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + const STATIC_CONTENT = 'This page errors after a cookies call' + + // Reveal the link to trigger a runtime prefetch + await act(async () => { + const linkToggle = await browser.elementByCss( + `input[data-prefetch="runtime"][data-link-accordion="/errors/error-after-cookies"]` + ) + await linkToggle.click() + }, [ + // Should include the shell + { + includes: STATIC_CONTENT, + }, + ]) + + if (!isNextDeploy) { + expect(next.cliOutput).toContain('Error: Kaboom') + } + + // Navigate to the page. We already have the paged cached. + // Even though the render errored, we shouldn't fetch it again. + await act(async () => { + await browser + .elementByCss(`a[href="/errors/error-after-cookies"]`) + .click() + }, 'no-requests') + + // After navigating, we should see the sync IO result that we omitted from the prefetch. + expect(await browser.elementById('intro').text()).toInclude( + STATIC_CONTENT + ) + expect(await browser.elementById('error-boundary').text()).toInclude( + 'Error boundary: An error occurred in the Server Components render' + ) + }) + }) +}) + +function defer(callback: () => Promise) { + return { + [Symbol.asyncDispose]: callback, + } +} diff --git a/test/e2e/app-dir/segment-cache/revalidation/app/refetch-on-new-base-tree/layout.tsx b/test/e2e/app-dir/segment-cache/revalidation/app/refetch-on-new-base-tree/layout.tsx index 1f0eef1102b602..7b6987174ee58c 100644 --- a/test/e2e/app-dir/segment-cache/revalidation/app/refetch-on-new-base-tree/layout.tsx +++ b/test/e2e/app-dir/segment-cache/revalidation/app/refetch-on-new-base-tree/layout.tsx @@ -10,16 +10,17 @@ export default function RefetchOnNewBaseTreeLayout({

    This demonstrates what happens when a link is prefetched using{' '} - {'prefetch={true}'} and the URL changes. Next.js should - re-prefetch the link in case the delta between the base tree and the - target tree has changed. + {'prefetch="unstable_forceStale"'} and the URL changes. + Next.js should re-prefetch the link in case the delta between the base + tree and the target tree has changed.

    Everything in this gray section is part of a shared layout. The links - below are prefetched using {'prefetch={true}'}. If the - first loaded page is "/refetch-on-new-base-tree/a", the prefetch for - this link will be empty, because there's no delta between the base - tree and the target tree. + below are prefetched using{' '} + {'prefetch="unstable_forceStale"'}. If the first loaded + page is "/refetch-on-new-base-tree/a", the prefetch for this link will + be empty, because there's no delta between the base tree and the + target tree.

    However, if you then navigate to page B, we should re-prefetch the @@ -48,10 +49,16 @@ export default function RefetchOnNewBaseTreeLayout({ because it was fully prefetched.

  • - + Page A - + Page B diff --git a/test/e2e/app-dir/segment-cache/revalidation/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/revalidation/components/link-accordion.tsx index a11b931348c6b5..5206fa1aae0e5c 100644 --- a/test/e2e/app-dir/segment-cache/revalidation/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/revalidation/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import Form from 'next/form' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' @@ -12,7 +12,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] }) { const [isVisible, setIsVisible] = useState(false) return ( @@ -92,6 +92,9 @@ export function ManualPrefetchLinkAccordion({ ) } +type Router = ReturnType +type PrefetchOptions = Parameters[1] + function ManualPrefetchLink({ href, children, @@ -109,8 +112,8 @@ function ManualPrefetchLink({ let didUnmount = false const pollPrefetch = () => { if (!didUnmount) { - // @ts-expect-error: onInvalidate is not yet part of public types router.prefetch(href, { + kind: 'auto' as PrefetchOptions['kind'], onInvalidate: pollPrefetch, }) } diff --git a/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts b/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts index aa8a1c3332231a..0ba07df51c9ac5 100644 --- a/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts +++ b/test/e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts @@ -272,7 +272,7 @@ describe('segment cache (revalidation)', () => { includes: 'Page B content', }, // Page A's content should not be prefetched because we're already on that - // page. When prefetching with `prefetch={true}`, we only prefetch the + // page. When prefetching with `prefetch='unstable_forceStale'`, we only prefetch the // delta between the current route and the target route. { includes: 'Page A content', diff --git a/test/e2e/app-dir/segment-cache/router-act.ts b/test/e2e/app-dir/segment-cache/router-act.ts index f18435454cee51..d73e4cfdb8277c 100644 --- a/test/e2e/app-dir/segment-cache/router-act.ts +++ b/test/e2e/app-dir/segment-cache/router-act.ts @@ -163,10 +163,19 @@ export function createRouterAct( const originalResponse = await page.request.fetch(request, { maxRedirects: 0, }) + + // WORKAROUND: + // intercepting responses with 'Transfer-Encoding: chunked' (used for streaming) + // seems to be problematic sometimes, making the browser error with `net::ERR_INCOMPLETE_CHUNKED_ENCODING`. + // In particular, this seems to happen when blocking a streaming navigation response. (but not always) + // Playwright buffers the whole body anyway, so we can remove the header to sidestep this. + const headers = originalResponse.headers() + delete headers['transfer-encoding'] + resolve({ text: await originalResponse.text(), body: await originalResponse.body(), - headers: originalResponse.headers(), + headers, status: originalResponse.status(), }) }), diff --git a/test/e2e/app-dir/segment-cache/search-params/app/search-params/page.tsx b/test/e2e/app-dir/segment-cache/search-params/app/search-params/page.tsx index 0a3e9818a70b8d..0dc847801e63c0 100644 --- a/test/e2e/app-dir/segment-cache/search-params/app/search-params/page.tsx +++ b/test/e2e/app-dir/segment-cache/search-params/app/search-params/page.tsx @@ -19,10 +19,10 @@ export default function SearchParamsPage({
  • - searchParam=b_full, prefetch=true + searchParam=b_full, prefetch="unstable_forceStale"
  • @@ -32,10 +32,10 @@ export default function SearchParamsPage({
  • - searchParam=d_full, prefetch=true + searchParam=d_full, prefetch="unstable_forceStale"
  • diff --git a/test/e2e/app-dir/segment-cache/search-params/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/search-params/components/link-accordion.tsx index 4bb0955e44c7f9..72dafb3b9d970c 100644 --- a/test/e2e/app-dir/segment-cache/search-params/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/search-params/components/link-accordion.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import Link, { type LinkProps } from 'next/link' import { useState } from 'react' export function LinkAccordion({ @@ -10,7 +10,7 @@ export function LinkAccordion({ }: { href: string children: React.ReactNode - prefetch?: boolean + prefetch?: LinkProps['prefetch'] }) { const [isVisible, setIsVisible] = useState(false) return ( diff --git a/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts b/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts index ef2269a8f52f1c..a86678fce7e72e 100644 --- a/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts +++ b/test/e2e/app-dir/segment-cache/search-params/segment-cache-search-params.test.ts @@ -67,7 +67,7 @@ describe('segment cache (search params)', () => { expect(await result.innerText()).toBe('Search param: c_PPR') }) - it('when fetching without PPR (e.g. prefetch={true}), includes the search params in the cache key', async () => { + it('when fetching without PPR (e.g. prefetch="unstable_forceStale"), includes the search params in the cache key', async () => { let act: ReturnType const browser = await next.browser('/search-params', { beforePageLoad(page) { @@ -75,7 +75,7 @@ describe('segment cache (search params)', () => { }, }) - // Prefetch a page with search param `b_full`. This link has prefetch={true} + // Prefetch a page with search param `b_full`. This link has prefetch='unstable_forceStale' // so it will fetch the entire page, including the search param. const revealB = await browser.elementByCss( 'input[data-link-accordion="/search-params/target-page?searchParam=b_full"]' @@ -91,7 +91,7 @@ describe('segment cache (search params)', () => { ) // Prefetch a link with a different search param, and without - // prefetch={true}. This must fetch a new shell, because it can't use the + // prefetch='unstable_forceStale'. This must fetch a new shell, because it can't use the // entry we fetched for `searchParam=b_full` (because that one wasn't a // shell — it included the search param). const revealA = await browser.elementByCss( @@ -106,7 +106,7 @@ describe('segment cache (search params)', () => { { includes: 'target-page-with-search-param' } ) - // Prefetch a different link using prefetch={true}. Again, this must issue + // Prefetch a different link using prefetch='unstable_forceStale'. Again, this must issue // a new request, because it's a full page prefetch and we haven't fetched // this particular search param before. // TODO: As an future optimization, if a navigation to this link occurs diff --git a/test/e2e/app-dir/segment-cache/staleness/app/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/page.tsx index f948420858f76c..aa6e9e530813d8 100644 --- a/test/e2e/app-dir/segment-cache/staleness/app/page.tsx +++ b/test/e2e/app-dir/segment-cache/staleness/app/page.tsx @@ -20,6 +20,16 @@ export default function Page() { Page with stale time of 10 minutes
    +
  • + + Page whose runtime prefetch has a stale time of 5 minutes + +
  • +
  • + + Page whose runtime prefetch has a stale time of 10 minutes + +
  • Page with dynamic data
  • diff --git a/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-10-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-10-minutes/page.tsx new file mode 100644 index 00000000000000..010962995bbd40 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-10-minutes/page.tsx @@ -0,0 +1,30 @@ +import { Suspense } from 'react' +import { unstable_cacheLife as cacheLife } from 'next/cache' +import { cookies } from 'next/headers' + +export default function Page() { + return ( + + + + ) +} + +async function RuntimePrefetchable() { + // Prevent this content from appearing in a regular prerender, + // But allow it to be included in a runtime prefetch. + await cookies() + + return ( + + + + ) +} + +async function Content() { + 'use cache' + await new Promise((resolve) => setTimeout(resolve, 0)) + cacheLife({ stale: 10 * 60 }) + return 'Content with stale time of 10 minutes' +} diff --git a/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-5-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-5-minutes/page.tsx new file mode 100644 index 00000000000000..214b50c911dc92 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-5-minutes/page.tsx @@ -0,0 +1,30 @@ +import { Suspense } from 'react' +import { unstable_cacheLife as cacheLife } from 'next/cache' +import { cookies } from 'next/headers' + +export default function Page() { + return ( + + + + ) +} + +async function RuntimePrefetchable() { + // Prevent this content from appearing in a regular prerender, + // But allow it to be included in a runtime prefetch. + await cookies() + + return ( + + + + ) +} + +async function Content() { + 'use cache' + await new Promise((resolve) => setTimeout(resolve, 0)) + cacheLife({ stale: 5 * 60 }) + return 'Content with stale time of 5 minutes' +} diff --git a/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx index 4b253eab3adf36..fd8f6781732ead 100644 --- a/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx +++ b/test/e2e/app-dir/segment-cache/staleness/components/link-accordion.tsx @@ -1,9 +1,17 @@ 'use client' -import Link from 'next/link' +import Link, { LinkProps } from 'next/link' import { useState } from 'react' -export function LinkAccordion({ href, children }) { +export function LinkAccordion({ + href, + children, + prefetch, +}: { + href: string + children: React.ReactNode + prefetch?: LinkProps['prefetch'] +}) { const [isVisible, setIsVisible] = useState(false) return ( <> @@ -14,7 +22,9 @@ export function LinkAccordion({ href, children }) { data-link-accordion={href} /> {isVisible ? ( - {children} + + {children} + ) : ( `${children} (link is hidden)` )} diff --git a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts index 8e3d16abe40275..331500951a92fd 100644 --- a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts +++ b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts @@ -79,6 +79,74 @@ describe('segment cache (staleness)', () => { ) }) + it('expires runtime prefetches when their stale time has elapsed', async () => { + let page: Playwright.Page + const browser = await next.browser('/', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + await page.clock.install() + + // Reveal the links to trigger a runtime prefetch + const toggle5MinutesLink = await browser.elementByCss( + 'input[data-link-accordion="/runtime-stale-5-minutes"]' + ) + const toggle10MinutesLink = await browser.elementByCss( + 'input[data-link-accordion="/runtime-stale-10-minutes"]' + ) + await act( + async () => { + await toggle5MinutesLink.click() + await browser.elementByCss('a[href="/runtime-stale-5-minutes"]') + }, + { + includes: 'Content with stale time of 5 minutes', + } + ) + await act( + async () => { + await toggle10MinutesLink.click() + await browser.elementByCss('a[href="/runtime-stale-10-minutes"]') + }, + { + includes: 'Content with stale time of 10 minutes', + } + ) + + // Hide the links + await toggle5MinutesLink.click() + await toggle10MinutesLink.click() + + // Fast forward 5 minutes and 1 millisecond + await page.clock.fastForward(5 * 60 * 1000 + 1) + + // Reveal the links again to trigger new prefetch tasks + await act( + async () => { + await toggle5MinutesLink.click() + await browser.elementByCss('a[href="/runtime-stale-5-minutes"]') + }, + // The page with a stale time of 5 minutes is requested again + // because its stale time elapsed. + { + includes: 'Content with stale time of 5 minutes', + } + ) + + await act( + async () => { + await toggle10MinutesLink.click() + await browser.elementByCss('a[href="/runtime-stale-10-minutes"]') + }, + // The page with a stale time of 10 minutes is *not* requested again + // because it's still fresh. + 'no-requests' + ) + }) + it('reuses dynamic data up to the staleTimes.dynamic threshold', async () => { let page: Playwright.Page const startDate = Date.now() diff --git a/test/e2e/next-link-errors/next-link-errors.test.ts b/test/e2e/next-link-errors/next-link-errors.test.ts index 6e47795198c5d6..10e41e4d5797f6 100644 --- a/test/e2e/next-link-errors/next-link-errors.test.ts +++ b/test/e2e/next-link-errors/next-link-errors.test.ts @@ -86,7 +86,7 @@ describe('next-link', () => { if (isNextDev) { await expect(browser).toDisplayRedbox(` { - "description": "Failed prop type: The prop \`prefetch\` expects a \`boolean | "auto"\` in \`\`, but got \`string\` instead. + "description": "Failed prop type: The prop \`prefetch\` expects a \`boolean | "auto" | "unstable_forceStale"\` in \`\`, but got \`string\` instead. Open your browser's console to view the Component stack trace.", "environmentLabel": null, "label": "Runtime Error",