.
This commit is contained in:
Generated
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
import { INTERCEPTION_ROUTE_MARKERS } from '../../../shared/lib/router/utils/interception-routes';
|
||||
import { isGroupSegment, DEFAULT_SEGMENT_KEY, PAGE_SEGMENT_KEY } from '../../../shared/lib/segment';
|
||||
import { matchSegment } from '../match-segments';
|
||||
const removeLeadingSlash = (segment)=>{
|
||||
return segment[0] === '/' ? segment.slice(1) : segment;
|
||||
};
|
||||
const segmentToPathname = (segment)=>{
|
||||
if (typeof segment === 'string') {
|
||||
// 'children' is not a valid path -- it's technically a parallel route that corresponds with the current segment's page
|
||||
// if we don't skip it, then the computed pathname might be something like `/children` which doesn't make sense.
|
||||
if (segment === 'children') return '';
|
||||
return segment;
|
||||
}
|
||||
return segment[1];
|
||||
};
|
||||
const segmentToSourcePagePathname = (segment)=>{
|
||||
if (typeof segment === 'string') {
|
||||
if (segment === 'children') return '';
|
||||
if (segment.startsWith(PAGE_SEGMENT_KEY)) return 'page';
|
||||
return segment;
|
||||
}
|
||||
const [paramName, , dynamicParamType] = segment;
|
||||
switch(dynamicParamType){
|
||||
case 'c':
|
||||
return `[...${paramName}]`;
|
||||
case 'ci(..)(..)':
|
||||
return `(..)(..)[...${paramName}]`;
|
||||
case 'ci(.)':
|
||||
return `(.)[...${paramName}]`;
|
||||
case 'ci(..)':
|
||||
return `(..)[...${paramName}]`;
|
||||
case 'ci(...)':
|
||||
return `(...)[...${paramName}]`;
|
||||
case 'oc':
|
||||
return `[[...${paramName}]]`;
|
||||
case 'd':
|
||||
return `[${paramName}]`;
|
||||
case 'di(..)(..)':
|
||||
return `(..)(..)[${paramName}]`;
|
||||
case 'di(.)':
|
||||
return `(.)[${paramName}]`;
|
||||
case 'di(..)':
|
||||
return `(..)[${paramName}]`;
|
||||
case 'di(...)':
|
||||
return `(...)[${paramName}]`;
|
||||
default:
|
||||
dynamicParamType;
|
||||
return `[${paramName}]`;
|
||||
}
|
||||
};
|
||||
function normalizeSegments(segments) {
|
||||
return segments.reduce((acc, segment)=>{
|
||||
segment = removeLeadingSlash(segment);
|
||||
if (segment === '' || isGroupSegment(segment)) {
|
||||
return acc;
|
||||
}
|
||||
return `${acc}/${segment}`;
|
||||
}, '') || '/';
|
||||
}
|
||||
export function extractPathFromFlightRouterState(flightRouterState) {
|
||||
const segment = Array.isArray(flightRouterState[0]) ? flightRouterState[0][1] : flightRouterState[0];
|
||||
if (segment === DEFAULT_SEGMENT_KEY || INTERCEPTION_ROUTE_MARKERS.some((m)=>segment.startsWith(m))) return undefined;
|
||||
if (segment.startsWith(PAGE_SEGMENT_KEY)) return '';
|
||||
const segments = [
|
||||
segmentToPathname(segment)
|
||||
];
|
||||
const parallelRoutes = flightRouterState[1] ?? {};
|
||||
const childrenPath = parallelRoutes.children ? extractPathFromFlightRouterState(parallelRoutes.children) : undefined;
|
||||
if (childrenPath !== undefined) {
|
||||
segments.push(childrenPath);
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(parallelRoutes)){
|
||||
if (key === 'children') continue;
|
||||
const childPath = extractPathFromFlightRouterState(value);
|
||||
if (childPath !== undefined) {
|
||||
segments.push(childPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizeSegments(segments);
|
||||
}
|
||||
function extractSourcePageSegmentsFromFlightRouterState(flightRouterState) {
|
||||
const segment = segmentToSourcePagePathname(flightRouterState[0]);
|
||||
if (segment === DEFAULT_SEGMENT_KEY) {
|
||||
return undefined;
|
||||
}
|
||||
if (segment === 'page') {
|
||||
return [
|
||||
segment
|
||||
];
|
||||
}
|
||||
const parallelRoutes = flightRouterState[1] ?? {};
|
||||
const childrenPath = parallelRoutes.children ? extractSourcePageSegmentsFromFlightRouterState(parallelRoutes.children) : undefined;
|
||||
if (childrenPath !== undefined) {
|
||||
return segment === '' ? childrenPath : [
|
||||
removeLeadingSlash(segment),
|
||||
...childrenPath
|
||||
];
|
||||
}
|
||||
for (const [key, value] of Object.entries(parallelRoutes)){
|
||||
if (key === 'children') continue;
|
||||
const childPath = extractSourcePageSegmentsFromFlightRouterState(value);
|
||||
if (childPath !== undefined) {
|
||||
return segment === '' ? childPath : [
|
||||
removeLeadingSlash(segment),
|
||||
...childPath
|
||||
];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export function extractSourcePageFromFlightRouterState(flightRouterState) {
|
||||
const sourcePageSegments = extractSourcePageSegmentsFromFlightRouterState(flightRouterState);
|
||||
return sourcePageSegments ? `/${sourcePageSegments.join('/')}` : undefined;
|
||||
}
|
||||
function computeChangedPathImpl(treeA, treeB) {
|
||||
const [segmentA, parallelRoutesA] = treeA;
|
||||
const [segmentB, parallelRoutesB] = treeB;
|
||||
const normalizedSegmentA = segmentToPathname(segmentA);
|
||||
const normalizedSegmentB = segmentToPathname(segmentB);
|
||||
if (INTERCEPTION_ROUTE_MARKERS.some((m)=>normalizedSegmentA.startsWith(m) || normalizedSegmentB.startsWith(m))) {
|
||||
return '';
|
||||
}
|
||||
if (!matchSegment(segmentA, segmentB)) {
|
||||
// once we find where the tree changed, we compute the rest of the path by traversing the tree
|
||||
return extractPathFromFlightRouterState(treeB) ?? '';
|
||||
}
|
||||
for(const parallelRouterKey in parallelRoutesA){
|
||||
if (parallelRoutesB[parallelRouterKey]) {
|
||||
const changedPath = computeChangedPathImpl(parallelRoutesA[parallelRouterKey], parallelRoutesB[parallelRouterKey]);
|
||||
if (changedPath !== null) {
|
||||
return `${segmentToPathname(segmentB)}/${changedPath}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function computeChangedPath(treeA, treeB) {
|
||||
const changedPath = computeChangedPathImpl(treeA, treeB);
|
||||
if (changedPath == null || changedPath === '/') {
|
||||
return changedPath;
|
||||
}
|
||||
// lightweight normalization to remove route groups
|
||||
return normalizeSegments(changedPath.split('/'));
|
||||
}
|
||||
/**
|
||||
* Recursively extracts dynamic parameters from FlightRouterState.
|
||||
*/ export function getSelectedParams(currentTree, params = {}) {
|
||||
const parallelRoutes = currentTree[1];
|
||||
for (const parallelRoute of Object.values(parallelRoutes)){
|
||||
const segment = parallelRoute[0];
|
||||
const isDynamicParameter = Array.isArray(segment);
|
||||
const segmentValue = isDynamicParameter ? segment[1] : segment;
|
||||
if (!segmentValue || segmentValue.startsWith(PAGE_SEGMENT_KEY)) continue;
|
||||
// Ensure catchAll and optional catchall are turned into an array
|
||||
const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc');
|
||||
if (isCatchAll) {
|
||||
params[segment[0]] = segment[1].split('/');
|
||||
} else if (isDynamicParameter) {
|
||||
params[segment[0]] = segment[1];
|
||||
}
|
||||
params = getSelectedParams(parallelRoute, params);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=compute-changed-path.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export function createHrefFromUrl(url, includeHash = true) {
|
||||
return url.pathname + url.search + (includeHash ? url.hash : '');
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-href-from-url.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/create-href-from-url.ts"],"sourcesContent":["export function createHrefFromUrl(\n url: Pick<URL, 'pathname' | 'search' | 'hash'>,\n includeHash: boolean = true\n): string {\n return url.pathname + url.search + (includeHash ? url.hash : '')\n}\n"],"names":["createHrefFromUrl","url","includeHash","pathname","search","hash"],"mappings":"AAAA,OAAO,SAASA,kBACdC,GAA8C,EAC9CC,cAAuB,IAAI;IAE3B,OAAOD,IAAIE,QAAQ,GAAGF,IAAIG,MAAM,GAAIF,CAAAA,cAAcD,IAAII,IAAI,GAAG,EAAC;AAChE","ignoreList":[0]}
|
||||
Generated
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
import { createHrefFromUrl } from './create-href-from-url';
|
||||
import { extractPathFromFlightRouterState } from './compute-changed-path';
|
||||
import { getFlightDataPartsFromPath } from '../../flight-data-helpers';
|
||||
import { createInitialCacheNodeForHydration } from './ppr-navigations';
|
||||
import { convertRootFlightRouterStateToRouteTree, getStaleAt, processRuntimePrefetchStream, writeDynamicRenderResponseIntoCache, writeStaticStageResponseIntoCache } from '../segment-cache/cache';
|
||||
import { FetchStrategy } from '../segment-cache/types';
|
||||
import { UnknownDynamicStaleTime, computeDynamicStaleAt } from '../segment-cache/bfcache';
|
||||
import { decodeStaticStage } from './fetch-server-response';
|
||||
import { discoverKnownRoute } from '../segment-cache/optimistic-routes';
|
||||
export function createInitialRouterState({ navigatedAt, initialRSCPayload, initialFlightStreamForCache, location }) {
|
||||
const { c: initialCanonicalUrlParts, f: initialFlightData, q: initialRenderedSearch, i: initialCouldBeIntercepted, S: initialSupportsPerSegmentPrefetching, s: initialStaleTime, l: initialStaticStageByteLength, h: initialHeadVaryParams, p: initialRuntimePrefetchStream, d: initialDynamicStaleTimeSeconds } = initialRSCPayload;
|
||||
// When initialized on the server, the canonical URL is provided as an array of parts.
|
||||
// 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]);
|
||||
const { tree: initialTree, seedData: initialSeedData, head: initialHead } = normalizedFlightData;
|
||||
// For the SSR render, seed data should always be available (we only send back a `null` response
|
||||
// in the case of a `loading` segment, pre-PPR.)
|
||||
const canonicalUrl = // location.href is read as the initial value for canonicalUrl in the browser
|
||||
// This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file.
|
||||
location ? createHrefFromUrl(location) : initialCanonicalUrl;
|
||||
// Convert the initial FlightRouterState into the RouteTree type.
|
||||
// NOTE: The metadataVaryPath isn't used for anything currently because the
|
||||
// head is embedded into the CacheNode tree, but eventually we'll lift it out
|
||||
// and store it on the top-level state object.
|
||||
//
|
||||
// TODO: For statically-generated-at-build-time HTML pages, the
|
||||
// FlightRouterState baked into the initial RSC payload won't have the
|
||||
// correct segment inlining hints (ParentInlinedIntoSelf, InlinedIntoChild)
|
||||
// because those are computed after the pre-render. The client will need to
|
||||
// fetch the correct hints from the route tree prefetch (/_tree) response
|
||||
// before acting on inlining decisions.
|
||||
const acc = {
|
||||
metadataVaryPath: null
|
||||
};
|
||||
const initialRouteTree = convertRootFlightRouterStateToRouteTree(initialTree, initialRenderedSearch, acc);
|
||||
const metadataVaryPath = acc.metadataVaryPath;
|
||||
const initialTask = createInitialCacheNodeForHydration(navigatedAt, initialRouteTree, initialSeedData, initialHead, computeDynamicStaleAt(navigatedAt, initialDynamicStaleTimeSeconds ?? UnknownDynamicStaleTime));
|
||||
// The following only applies in the browser (location !== null) since neither
|
||||
// route learning nor segment cache state persists from SSR to client.
|
||||
if (location !== null && metadataVaryPath !== null) {
|
||||
// Learn the route pattern so we can predict it for future navigations.
|
||||
discoverKnownRoute(Date.now(), location.pathname, null, null, initialRouteTree, metadataVaryPath, initialCouldBeIntercepted, canonicalUrl, initialSupportsPerSegmentPrefetching, false // hasDynamicRewrite
|
||||
);
|
||||
// Write the initial seed data into the segment cache so subsequent
|
||||
// navigations to the initial page can serve cached segments instantly.
|
||||
if (initialSeedData !== null && initialStaleTime !== undefined) {
|
||||
if (initialStaticStageByteLength !== undefined && initialFlightStreamForCache != null) {
|
||||
// Partially static page — truncate the cloned Flight stream at the
|
||||
// static stage byte boundary, decode, and cache the static subset.
|
||||
decodeStaticStage(initialFlightStreamForCache, initialStaticStageByteLength, undefined).then(async (staticStageResponse)=>{
|
||||
const now = Date.now();
|
||||
const staleAt = await getStaleAt(now, staticStageResponse.s);
|
||||
writeStaticStageResponseIntoCache(now, staticStageResponse.f, undefined, staticStageResponse.h, staleAt, initialTree, initialRenderedSearch, true // isResponsePartial
|
||||
);
|
||||
}).catch(()=>{
|
||||
// The static stage processing failed. Not fatal — the page
|
||||
// rendered normally, we just won't write into the cache.
|
||||
});
|
||||
} else {
|
||||
// Fully static page — cache the entire decoded seed data as-is. We're
|
||||
// not using the initial response here (which would allow us to combine
|
||||
// the two branches) to avoid unnecessary decoding of the Flight data,
|
||||
// since we can just take the seed data that we already decoded during
|
||||
// hydration and write it into the cache directly.
|
||||
const now = Date.now();
|
||||
getStaleAt(now, initialStaleTime).then((staleAt)=>{
|
||||
writeStaticStageResponseIntoCache(now, initialFlightData, undefined, initialHeadVaryParams, staleAt, initialTree, initialRenderedSearch, false // isResponsePartial
|
||||
);
|
||||
}).catch(()=>{
|
||||
// The static stage processing failed. Not fatal — the page
|
||||
// rendered normally, we just won't write into the cache.
|
||||
});
|
||||
// Cancel the stream clone — fully static path doesn't need it.
|
||||
initialFlightStreamForCache?.cancel();
|
||||
}
|
||||
} else {
|
||||
// No caching — cancel the unused stream clone.
|
||||
initialFlightStreamForCache?.cancel();
|
||||
}
|
||||
// If the initial RSC payload includes an embedded runtime prefetch stream,
|
||||
// decode it and write the runtime data into the segment cache. This allows
|
||||
// subsequent navigations to serve runtime-prefetchable content from cache
|
||||
// without a separate prefetch request.
|
||||
if (initialRuntimePrefetchStream != null) {
|
||||
processRuntimePrefetchStream(Date.now(), initialRuntimePrefetchStream, initialTree, initialRenderedSearch).then((processed)=>{
|
||||
if (processed !== null) {
|
||||
writeDynamicRenderResponseIntoCache(Date.now(), FetchStrategy.PPRRuntime, processed.flightDatas, processed.buildId, processed.isResponsePartial, processed.headVaryParams, processed.staleAt, processed.navigationSeed, null);
|
||||
}
|
||||
}).catch(()=>{
|
||||
// Runtime prefetch cache write failed. Not fatal — the page rendered
|
||||
// normally, we just won't cache runtime data.
|
||||
});
|
||||
}
|
||||
}
|
||||
// NOTE: We intentionally don't check if any data needs to be fetched from the
|
||||
// server. We assume the initial hydration payload is sufficient to render
|
||||
// the page.
|
||||
//
|
||||
// The completeness of the initial data is an important property that we rely
|
||||
// on as a last-ditch mechanism for recovering the app; we must always be able
|
||||
// to reload a fresh HTML document to get to a consistent state.
|
||||
//
|
||||
// In the future, there may be cases where the server intentionally sends
|
||||
// partial data and expects the client to fill in the rest, in which case this
|
||||
// logic may change. (There already is a similar case where the server sends
|
||||
// _no_ hydration data in the HTML document at all, and the client fetches it
|
||||
// separately, but that's different because we still end up hydrating with a
|
||||
// complete tree.)
|
||||
const initialState = {
|
||||
tree: initialTask.route,
|
||||
cache: initialTask.node,
|
||||
pushRef: {
|
||||
pendingPush: false,
|
||||
mpaNavigation: false,
|
||||
// First render needs to preserve the previous window.history.state
|
||||
// to avoid it being overwritten on navigation back/forward with MPA Navigation.
|
||||
preserveCustomHistoryState: true
|
||||
},
|
||||
focusAndScrollRef: {
|
||||
scrollRef: null,
|
||||
forceScroll: false,
|
||||
onlyHashChange: false,
|
||||
hashFragment: null
|
||||
},
|
||||
canonicalUrl,
|
||||
renderedSearch: initialRenderedSearch,
|
||||
// the || operator is intentional, the pathname can be an empty string
|
||||
nextUrl: (extractPathFromFlightRouterState(initialTree) || location?.pathname) ?? null,
|
||||
previousNextUrl: null,
|
||||
debugInfo: null
|
||||
};
|
||||
return initialState;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-initial-router-state.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment';
|
||||
export function createRouterCacheKey(segment, withoutSearchParameters = false) {
|
||||
// if the segment is an array, it means it's a dynamic segment
|
||||
// for example, ['lang', 'en', 'd']. We need to convert it to a string to store it as a cache node key.
|
||||
if (Array.isArray(segment)) {
|
||||
return `${segment[0]}|${segment[1]}|${segment[2]}`;
|
||||
}
|
||||
// Page segments might have search parameters, ie __PAGE__?foo=bar
|
||||
// When `withoutSearchParameters` is true, we only want to return the page segment
|
||||
if (withoutSearchParameters && segment.startsWith(PAGE_SEGMENT_KEY)) {
|
||||
return PAGE_SEGMENT_KEY;
|
||||
}
|
||||
return segment;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-router-cache-key.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/create-router-cache-key.ts"],"sourcesContent":["import type { Segment } from '../../../shared/lib/app-router-types'\nimport { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'\n\nexport function createRouterCacheKey(\n segment: Segment,\n withoutSearchParameters: boolean = false\n) {\n // if the segment is an array, it means it's a dynamic segment\n // for example, ['lang', 'en', 'd']. We need to convert it to a string to store it as a cache node key.\n if (Array.isArray(segment)) {\n return `${segment[0]}|${segment[1]}|${segment[2]}`\n }\n\n // Page segments might have search parameters, ie __PAGE__?foo=bar\n // When `withoutSearchParameters` is true, we only want to return the page segment\n if (withoutSearchParameters && segment.startsWith(PAGE_SEGMENT_KEY)) {\n return PAGE_SEGMENT_KEY\n }\n\n return segment\n}\n"],"names":["PAGE_SEGMENT_KEY","createRouterCacheKey","segment","withoutSearchParameters","Array","isArray","startsWith"],"mappings":"AACA,SAASA,gBAAgB,QAAQ,8BAA6B;AAE9D,OAAO,SAASC,qBACdC,OAAgB,EAChBC,0BAAmC,KAAK;IAExC,8DAA8D;IAC9D,uGAAuG;IACvG,IAAIC,MAAMC,OAAO,CAACH,UAAU;QAC1B,OAAO,GAAGA,OAAO,CAAC,EAAE,CAAC,CAAC,EAAEA,OAAO,CAAC,EAAE,CAAC,CAAC,EAAEA,OAAO,CAAC,EAAE,EAAE;IACpD;IAEA,kEAAkE;IAClE,kFAAkF;IAClF,IAAIC,2BAA2BD,QAAQI,UAAU,CAACN,mBAAmB;QACnE,OAAOA;IACT;IAEA,OAAOE;AACT","ignoreList":[0]}
|
||||
Generated
Vendored
+441
@@ -0,0 +1,441 @@
|
||||
'use client';
|
||||
// TODO: Explicitly import from client.browser
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createFromReadableStream as createFromReadableStreamBrowser, createFromFetch as createFromFetchBrowser } from 'react-server-dom-webpack/client';
|
||||
import { InvariantError } from '../../../shared/lib/invariant-error';
|
||||
import { NEXT_ROUTER_STATE_TREE_HEADER, NEXT_RSC_UNION_QUERY, NEXT_URL, RSC_HEADER, RSC_CONTENT_TYPE_HEADER, NEXT_HMR_REFRESH_HEADER, NEXT_DID_POSTPONE_HEADER, NEXT_HTML_REQUEST_ID_HEADER, NEXT_REQUEST_ID_HEADER } from '../app-router-headers';
|
||||
import { callServer } from '../../app-call-server';
|
||||
import { findSourceMapURL } from '../../app-find-source-map-url';
|
||||
import { normalizeFlightData, prepareFlightRouterStateForRequest } from '../../flight-data-helpers';
|
||||
import { setCacheBustingSearchParam } from './set-cache-busting-search-param';
|
||||
import { urlToUrlWithoutFlightMarker } from '../../route-params';
|
||||
import { getDeploymentId } from '../../../shared/lib/deployment-id';
|
||||
import { getNavigationBuildId } from '../../navigation-build-id';
|
||||
import { NEXT_NAV_DEPLOYMENT_ID_HEADER } from '../../../lib/constants';
|
||||
import { stripIsPartialByte } from '../segment-cache/cache';
|
||||
import { UnknownDynamicStaleTime } from '../segment-cache/bfcache';
|
||||
const createFromReadableStream = createFromReadableStreamBrowser;
|
||||
const createFromFetch = createFromFetchBrowser;
|
||||
let createDebugChannel;
|
||||
if (process.env.__NEXT_DEV_SERVER && process.env.__NEXT_REACT_DEBUG_CHANNEL) {
|
||||
createDebugChannel = require('../../dev/debug-channel').createDebugChannel;
|
||||
}
|
||||
function doMpaNavigation(url) {
|
||||
return urlToUrlWithoutFlightMarker(new URL(url, location.origin)).toString();
|
||||
}
|
||||
let isPageUnloading = false;
|
||||
if (typeof window !== 'undefined') {
|
||||
// Track when the page is unloading, e.g. due to reloading the page or
|
||||
// performing hard navigations. This allows us to suppress error logging when
|
||||
// the browser cancels in-flight requests during page unload.
|
||||
window.addEventListener('pagehide', ()=>{
|
||||
isPageUnloading = true;
|
||||
});
|
||||
// Reset the flag on pageshow, e.g. when navigating back and the JavaScript
|
||||
// execution context is restored by the browser.
|
||||
window.addEventListener('pageshow', ()=>{
|
||||
isPageUnloading = false;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Fetch the flight data for the provided url. Takes in the current router state
|
||||
* to decide what to render server-side.
|
||||
*/ export async function fetchServerResponse(url, options) {
|
||||
const { flightRouterState, nextUrl } = options;
|
||||
const headers = {
|
||||
// Enable flight response
|
||||
[RSC_HEADER]: '1',
|
||||
// Provide the current router state
|
||||
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(flightRouterState, options.isHmrRefresh)
|
||||
};
|
||||
if (process.env.NODE_ENV === 'development' && options.isHmrRefresh) {
|
||||
headers[NEXT_HMR_REFRESH_HEADER] = '1';
|
||||
}
|
||||
if (nextUrl) {
|
||||
headers[NEXT_URL] = nextUrl;
|
||||
}
|
||||
// In static export mode, we need to modify the URL to request the .txt file,
|
||||
// but we should preserve the original URL for the canonical URL and error handling.
|
||||
const originalUrl = url;
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
|
||||
// In "output: export" mode, we can't rely on headers to distinguish
|
||||
// between HTML and RSC requests. Instead, we append an extra prefix
|
||||
// to the request.
|
||||
url = new URL(url);
|
||||
if (url.pathname.endsWith('/')) {
|
||||
url.pathname += 'index.txt';
|
||||
} else {
|
||||
url.pathname += '.txt';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Typically, during a navigation, we decode the response using Flight's
|
||||
// `createFromFetch` API, which accepts a `fetch` promise.
|
||||
// TODO: Remove this check once the old PPR flag is removed
|
||||
const isLegacyPPR = process.env.__NEXT_PPR && !process.env.__NEXT_CACHE_COMPONENTS;
|
||||
const shouldImmediatelyDecode = !isLegacyPPR;
|
||||
const res = await createFetch(url, headers, 'auto', shouldImmediatelyDecode);
|
||||
const responseUrl = urlToUrlWithoutFlightMarker(new URL(res.url));
|
||||
const canonicalUrl = res.redirected ? responseUrl : originalUrl;
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
const interception = !!res.headers.get('vary')?.includes(NEXT_URL);
|
||||
const postponed = !!res.headers.get(NEXT_DID_POSTPONE_HEADER);
|
||||
let isFlightResponse = contentType.startsWith(RSC_CONTENT_TYPE_HEADER);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
|
||||
if (!isFlightResponse) {
|
||||
isFlightResponse = contentType.startsWith('text/plain');
|
||||
}
|
||||
}
|
||||
}
|
||||
// If fetch returns something different than flight response handle it like a mpa navigation
|
||||
// If the fetch was not 200, we also handle it like a mpa navigation
|
||||
if (!isFlightResponse || !res.ok || !res.body) {
|
||||
// in case the original URL came with a hash, preserve it before redirecting to the new URL
|
||||
if (url.hash) {
|
||||
responseUrl.hash = url.hash;
|
||||
}
|
||||
return doMpaNavigation(responseUrl.toString());
|
||||
}
|
||||
// We may navigate to a page that requires a different Webpack runtime.
|
||||
// In prod, every page will have the same Webpack runtime.
|
||||
// In dev, the Webpack runtime is minimal for each page.
|
||||
// We need to ensure the Webpack runtime is updated before executing client-side JS of the new page.
|
||||
// TODO: This needs to happen in the Flight Client.
|
||||
// Or Webpack needs to include the runtime update in the Flight response as
|
||||
// a blocking script.
|
||||
if (process.env.NODE_ENV !== 'production' && !process.env.TURBOPACK) {
|
||||
await require('../../dev/hot-reloader/app/hot-reloader-app').waitForWebpackRuntimeHotUpdate();
|
||||
}
|
||||
let flightResponsePromise = res.flightResponsePromise;
|
||||
if (flightResponsePromise === null) {
|
||||
// Typically, `createFetch` would have already started decoding the
|
||||
// Flight response. If it hasn't, though, we need to decode it now.
|
||||
// TODO: This should only be reachable if legacy PPR is enabled (i.e. PPR
|
||||
// without Cache Components). Remove this branch once legacy PPR
|
||||
// is deleted.
|
||||
flightResponsePromise = createFromNextReadableStream(res.body, headers, {
|
||||
allowPartialStream: postponed
|
||||
});
|
||||
}
|
||||
const [flightResponse, cacheData] = await Promise.all([
|
||||
flightResponsePromise,
|
||||
res.cacheData
|
||||
]);
|
||||
if ((res.headers.get(NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? flightResponse.b) !== getNavigationBuildId()) {
|
||||
// The server build does not match the client build.
|
||||
return doMpaNavigation(res.url);
|
||||
}
|
||||
const normalizedFlightData = normalizeFlightData(flightResponse.f);
|
||||
if (typeof normalizedFlightData === 'string') {
|
||||
return doMpaNavigation(normalizedFlightData);
|
||||
}
|
||||
const staticStageData = cacheData !== null ? await resolveStaticStageData(cacheData, flightResponse, headers) : null;
|
||||
return {
|
||||
flightData: normalizedFlightData,
|
||||
canonicalUrl: canonicalUrl,
|
||||
// TODO: We should be able to read this from the rewrite header, not the
|
||||
// Flight response. Theoretically they should always agree, but there are
|
||||
// currently some cases where it's incorrect for interception routes. We
|
||||
// can always trust the value in the response body. However, per-segment
|
||||
// prefetch responses don't embed the value in the body; they rely on the
|
||||
// header alone. So we need to investigate why the header is sometimes
|
||||
// wrong for interception routes.
|
||||
renderedSearch: flightResponse.q,
|
||||
couldBeIntercepted: interception,
|
||||
supportsPerSegmentPrefetching: flightResponse.S,
|
||||
postponed,
|
||||
// The dynamicStaleTime is only present in the response body when
|
||||
// a page exports unstable_dynamicStaleTime and this is a dynamic render.
|
||||
// When absent (UnknownDynamicStaleTime), the client falls back to the
|
||||
// global DYNAMIC_STALETIME_MS. The value is in seconds.
|
||||
dynamicStaleTime: flightResponse.d ?? UnknownDynamicStaleTime,
|
||||
staticStageData,
|
||||
runtimePrefetchStream: flightResponse.p ?? null,
|
||||
responseHeaders: res.headers,
|
||||
debugInfo: flightResponsePromise._debugInfo ?? null
|
||||
};
|
||||
} catch (err) {
|
||||
if (!isPageUnloading) {
|
||||
console.error(`Failed to fetch RSC payload for ${originalUrl}. Falling back to browser navigation.`, err);
|
||||
}
|
||||
// If fetch fails handle it like a mpa navigation
|
||||
// TODO-APP: Add a test for the case where a CORS request fails, e.g. external url redirect coming from the response.
|
||||
// See https://github.com/vercel/next.js/issues/43605#issuecomment-1451617521 for a reproduction.
|
||||
return originalUrl.toString();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Strips the leading isPartial byte from an RSC navigation response and
|
||||
* clones the body for segment cache extraction.
|
||||
*
|
||||
* When cache components is enabled, the server prepends a single byte:
|
||||
* '~' (0x7e) for partial, '#' (0x23) for complete. This must be stripped
|
||||
* before Flight decoding because it's not valid RSC data. The body is
|
||||
* cloned before Flight can consume it so the clone is available for later use.
|
||||
*
|
||||
* When cache components is disabled, returns the original response with
|
||||
* cacheData: null.
|
||||
*/ export async function processFetch(response) {
|
||||
if (process.env.__NEXT_CACHE_COMPONENTS) {
|
||||
if (!response.body) {
|
||||
throw Object.defineProperty(new InvariantError('Expected RSC navigation response to have a body'), "__NEXT_ERROR_CODE", {
|
||||
value: "E1088",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const { stream, isPartial } = await stripIsPartialByte(response.body);
|
||||
let responseStream;
|
||||
let cacheData;
|
||||
if (process.env.__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS) {
|
||||
const [stream1, stream2] = stream.tee();
|
||||
responseStream = stream1;
|
||||
cacheData = {
|
||||
isResponsePartial: isPartial,
|
||||
responseBodyClone: stream2
|
||||
};
|
||||
} else {
|
||||
responseStream = stream;
|
||||
cacheData = {
|
||||
isResponsePartial: isPartial
|
||||
};
|
||||
}
|
||||
const strippedResponse = new Response(responseStream, {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
// The Response constructor doesn't preserve `url` or `redirected` from
|
||||
// the original. We need both: `url` for React DevTools and `redirected`
|
||||
// for the redirect replay logic below.
|
||||
Object.defineProperty(strippedResponse, 'url', {
|
||||
value: response.url
|
||||
});
|
||||
Object.defineProperty(strippedResponse, 'redirected', {
|
||||
value: response.redirected
|
||||
});
|
||||
return {
|
||||
response: strippedResponse,
|
||||
cacheData
|
||||
};
|
||||
}
|
||||
return {
|
||||
response,
|
||||
cacheData: null
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolves the static stage response from the raw `processFetch` outputs and
|
||||
* the decoded flight response, for writing into the segment cache.
|
||||
*
|
||||
* - Fully static: use the decoded flight response as-is, no truncation needed.
|
||||
* - Not fully static + `l` field: truncate the body clone at the static stage
|
||||
* byte boundary and decode.
|
||||
* - Otherwise: no cache-worthy data.
|
||||
*/ export async function resolveStaticStageData(cacheData, flightResponse, headers) {
|
||||
const { isResponsePartial, responseBodyClone } = cacheData;
|
||||
if (responseBodyClone) {
|
||||
if (!isResponsePartial) {
|
||||
// Fully static — cache the entire decoded response as-is.
|
||||
responseBodyClone.cancel();
|
||||
return {
|
||||
response: flightResponse,
|
||||
isResponsePartial: false
|
||||
};
|
||||
}
|
||||
if (flightResponse.l !== undefined) {
|
||||
// Partially static — truncate the body clone at the byte boundary and
|
||||
// decode it.
|
||||
const response = await decodeStaticStage(responseBodyClone, flightResponse.l, headers);
|
||||
return {
|
||||
response,
|
||||
isResponsePartial: true
|
||||
};
|
||||
}
|
||||
// No caching — cancel the unused clone.
|
||||
responseBodyClone.cancel();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Truncates a Flight stream clone at the given byte boundary and decodes the
|
||||
* static stage prefix. Used by both the navigation path and the initial HTML
|
||||
* hydration path.
|
||||
*/ export async function decodeStaticStage(responseBodyClone, staticStageByteLengthPromise, headers) {
|
||||
const staticStageByteLength = await staticStageByteLengthPromise;
|
||||
const truncatedStream = truncateStream(responseBodyClone, staticStageByteLength);
|
||||
return createFromNextReadableStream(truncatedStream, headers, {
|
||||
allowPartialStream: true
|
||||
});
|
||||
}
|
||||
export async function createFetch(url, headers, fetchPriority, shouldImmediatelyDecode, signal) {
|
||||
// TODO: In output: "export" mode, the headers do nothing. Omit them (and the
|
||||
// cache busting search param) from the request so they're
|
||||
// maximally cacheable.
|
||||
if (process.env.__NEXT_TEST_MODE && fetchPriority !== null) {
|
||||
headers['Next-Test-Fetch-Priority'] = fetchPriority;
|
||||
}
|
||||
const deploymentId = getDeploymentId();
|
||||
if (deploymentId) {
|
||||
headers['x-deployment-id'] = deploymentId;
|
||||
}
|
||||
if (process.env.__NEXT_DEV_SERVER) {
|
||||
if (self.__next_r) {
|
||||
headers[NEXT_HTML_REQUEST_ID_HEADER] = self.__next_r;
|
||||
}
|
||||
// Create a new request ID for the server action request. The server uses
|
||||
// this to tag debug information sent via WebSocket to the client, which
|
||||
// then routes those chunks to the debug channel associated with this ID.
|
||||
headers[NEXT_REQUEST_ID_HEADER] = crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
|
||||
}
|
||||
const fetchOptions = {
|
||||
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
|
||||
credentials: 'same-origin',
|
||||
headers,
|
||||
priority: fetchPriority || undefined,
|
||||
signal
|
||||
};
|
||||
// `fetchUrl` is slightly different from `url` because we add a cache-busting
|
||||
// search param to it. This should not leak outside of this function, so we
|
||||
// track them separately.
|
||||
let fetchUrl = new URL(url);
|
||||
setCacheBustingSearchParam(fetchUrl, headers);
|
||||
let processed = fetch(fetchUrl, fetchOptions).then(processFetch);
|
||||
let fetchPromise = processed.then(({ response })=>response);
|
||||
// Immediately pass the fetch promise to the Flight client so that the debug
|
||||
// info includes the latency from the client to the server. The internal timer
|
||||
// in React starts as soon as `createFromFetch` is called.
|
||||
//
|
||||
// The only case where we don't do this is during a prefetch, because a
|
||||
// top-level prefetch response never blocks a navigation; if it hasn't already
|
||||
// been written into the cache by the time the navigation happens, the router
|
||||
// will go straight to a dynamic request.
|
||||
let flightResponsePromise = shouldImmediatelyDecode ? createFromNextFetch(fetchPromise, headers) : null;
|
||||
let browserResponse = await fetchPromise;
|
||||
// If the server responds with a redirect (e.g. 307), and the redirected
|
||||
// location does not contain the cache busting search param set in the
|
||||
// original request, the response is likely invalid — when following the
|
||||
// redirect, the browser forwards the request headers, but since the cache
|
||||
// busting search param is missing, the server will reject the request due to
|
||||
// a mismatch.
|
||||
//
|
||||
// Ideally, we would be able to intercept the redirect response and perform it
|
||||
// manually, instead of letting the browser automatically follow it, but this
|
||||
// is not allowed by the fetch API.
|
||||
//
|
||||
// So instead, we must "replay" the redirect by fetching the new location
|
||||
// again, but this time we'll append the cache busting search param to prevent
|
||||
// a mismatch.
|
||||
//
|
||||
// TODO: We can optimize Next.js's built-in middleware APIs by returning a
|
||||
// custom status code, to prevent the browser from automatically following it.
|
||||
//
|
||||
// This does not affect Server Action-based redirects; those are encoded
|
||||
// differently, as part of the Flight body. It only affects redirects that
|
||||
// occur in a middleware or a third-party proxy.
|
||||
let redirected = browserResponse.redirected;
|
||||
if (process.env.__NEXT_CLIENT_VALIDATE_RSC_REQUEST_HEADERS) {
|
||||
// This is to prevent a redirect loop. Same limit used by Chrome.
|
||||
const MAX_REDIRECTS = 20;
|
||||
for(let n = 0; n < MAX_REDIRECTS; n++){
|
||||
if (!browserResponse.redirected) {
|
||||
break;
|
||||
}
|
||||
const responseUrl = new URL(browserResponse.url, fetchUrl);
|
||||
if (responseUrl.origin !== fetchUrl.origin) {
|
||||
break;
|
||||
}
|
||||
if (responseUrl.searchParams.get(NEXT_RSC_UNION_QUERY) === fetchUrl.searchParams.get(NEXT_RSC_UNION_QUERY)) {
|
||||
break;
|
||||
}
|
||||
// The RSC request was redirected. Assume the response is invalid.
|
||||
//
|
||||
// Append the cache busting search param to the redirected URL and
|
||||
// fetch again.
|
||||
// TODO: We should abort the previous request.
|
||||
fetchUrl = new URL(responseUrl);
|
||||
setCacheBustingSearchParam(fetchUrl, headers);
|
||||
processed = fetch(fetchUrl, fetchOptions).then(processFetch);
|
||||
fetchPromise = processed.then(({ response })=>response);
|
||||
flightResponsePromise = shouldImmediatelyDecode ? createFromNextFetch(fetchPromise, headers) : null;
|
||||
browserResponse = await fetchPromise;
|
||||
// We just performed a manual redirect, so this is now true.
|
||||
redirected = true;
|
||||
}
|
||||
}
|
||||
// Remove the cache busting search param from the response URL, to prevent it
|
||||
// from leaking outside of this function.
|
||||
const responseUrl = new URL(browserResponse.url, fetchUrl);
|
||||
responseUrl.searchParams.delete(NEXT_RSC_UNION_QUERY);
|
||||
const rscResponse = {
|
||||
url: responseUrl.href,
|
||||
// This is true if any redirects occurred, either automatically by the
|
||||
// browser, or manually by us. So it's different from
|
||||
// `browserResponse.redirected`, which only tells us whether the browser
|
||||
// followed a redirect, and only for the last response in the chain.
|
||||
redirected,
|
||||
// These can be copied from the last browser response we received. We
|
||||
// intentionally only expose the subset of fields that are actually used
|
||||
// elsewhere in the codebase.
|
||||
ok: browserResponse.ok,
|
||||
headers: browserResponse.headers,
|
||||
body: browserResponse.body,
|
||||
status: browserResponse.status,
|
||||
// This is the exact promise returned by `createFromFetch`. It contains
|
||||
// debug information that we need to transfer to any derived promises that
|
||||
// are later rendered by React.
|
||||
flightResponsePromise: flightResponsePromise,
|
||||
cacheData: processed.then(({ cacheData })=>cacheData)
|
||||
};
|
||||
return rscResponse;
|
||||
}
|
||||
export function createFromNextReadableStream(flightStream, requestHeaders, options) {
|
||||
return createFromReadableStream(flightStream, {
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
debugChannel: createDebugChannel && createDebugChannel(requestHeaders),
|
||||
unstable_allowPartialStream: options?.allowPartialStream
|
||||
});
|
||||
}
|
||||
function createFromNextFetch(promiseForResponse, requestHeaders) {
|
||||
return createFromFetch(promiseForResponse, {
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
debugChannel: createDebugChannel && createDebugChannel(requestHeaders)
|
||||
});
|
||||
}
|
||||
function truncateStream(stream, byteLength) {
|
||||
const reader = stream.getReader();
|
||||
let remaining = byteLength;
|
||||
return new ReadableStream({
|
||||
async pull (controller) {
|
||||
if (remaining <= 0) {
|
||||
reader.cancel();
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
if (value.byteLength <= remaining) {
|
||||
controller.enqueue(value);
|
||||
remaining -= value.byteLength;
|
||||
} else {
|
||||
controller.enqueue(value.subarray(0, remaining));
|
||||
remaining = 0;
|
||||
reader.cancel();
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
cancel () {
|
||||
reader.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=fetch-server-response.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
import { PrefetchHint } from '../../../shared/lib/app-router-types';
|
||||
export function isNavigatingToNewRootLayout(currentTree, nextTree) {
|
||||
// Compare segments
|
||||
const currentTreeSegment = currentTree[0];
|
||||
const nextTreeSegment = nextTree.segment;
|
||||
// If any segment is different before we find the root layout, the root layout has changed.
|
||||
// E.g. /same/(group1)/layout.js -> /same/(group2)/layout.js
|
||||
// First segment is 'same' for both, keep looking. (group1) changed to (group2) before the root layout was found, it must have changed.
|
||||
if (Array.isArray(currentTreeSegment) && Array.isArray(nextTreeSegment)) {
|
||||
// Compare dynamic param name and type but ignore the value, different values would not affect the current root layout
|
||||
// /[name] - /slug1 and /slug2, both values (slug1 & slug2) still has the same layout /[name]/layout.js
|
||||
if (currentTreeSegment[0] !== nextTreeSegment[0] || currentTreeSegment[2] !== nextTreeSegment[2]) {
|
||||
return true;
|
||||
}
|
||||
} else if (currentTreeSegment !== nextTreeSegment) {
|
||||
return true;
|
||||
}
|
||||
// Current tree root layout found
|
||||
const currentIsRootLayout = ((currentTree[4] ?? 0) & PrefetchHint.IsRootLayout) !== 0;
|
||||
const nextIsRootLayout = (nextTree.prefetchHints & PrefetchHint.IsRootLayout) !== 0;
|
||||
if (currentIsRootLayout) {
|
||||
// If the next tree doesn't have the root layout flag, it must have changed.
|
||||
return !nextIsRootLayout;
|
||||
}
|
||||
// Current tree didn't have its root layout here, must have changed.
|
||||
if (nextIsRootLayout) {
|
||||
return true;
|
||||
}
|
||||
const slots = nextTree.slots;
|
||||
const currentTreeChildren = currentTree[1];
|
||||
if (slots !== null) {
|
||||
for(const slot in slots){
|
||||
const nextTreeChild = slots[slot];
|
||||
const currentTreeChild = currentTreeChildren[slot];
|
||||
if (currentTreeChild === undefined || isNavigatingToNewRootLayout(currentTreeChild, nextTreeChild)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=is-navigating-to-new-root-layout.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/is-navigating-to-new-root-layout.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../shared/lib/app-router-types'\nimport { PrefetchHint } from '../../../shared/lib/app-router-types'\nimport type { RouteTree } from '../segment-cache/cache'\n\nexport function isNavigatingToNewRootLayout(\n currentTree: FlightRouterState,\n nextTree: RouteTree\n): boolean {\n // Compare segments\n const currentTreeSegment = currentTree[0]\n const nextTreeSegment = nextTree.segment\n\n // If any segment is different before we find the root layout, the root layout has changed.\n // E.g. /same/(group1)/layout.js -> /same/(group2)/layout.js\n // First segment is 'same' for both, keep looking. (group1) changed to (group2) before the root layout was found, it must have changed.\n if (Array.isArray(currentTreeSegment) && Array.isArray(nextTreeSegment)) {\n // Compare dynamic param name and type but ignore the value, different values would not affect the current root layout\n // /[name] - /slug1 and /slug2, both values (slug1 & slug2) still has the same layout /[name]/layout.js\n if (\n currentTreeSegment[0] !== nextTreeSegment[0] ||\n currentTreeSegment[2] !== nextTreeSegment[2]\n ) {\n return true\n }\n } else if (currentTreeSegment !== nextTreeSegment) {\n return true\n }\n\n // Current tree root layout found\n const currentIsRootLayout =\n ((currentTree[4] ?? 0) & PrefetchHint.IsRootLayout) !== 0\n const nextIsRootLayout =\n (nextTree.prefetchHints & PrefetchHint.IsRootLayout) !== 0\n if (currentIsRootLayout) {\n // If the next tree doesn't have the root layout flag, it must have changed.\n return !nextIsRootLayout\n }\n // Current tree didn't have its root layout here, must have changed.\n if (nextIsRootLayout) {\n return true\n }\n\n const slots = nextTree.slots\n const currentTreeChildren = currentTree[1]\n if (slots !== null) {\n for (const slot in slots) {\n const nextTreeChild = slots[slot]\n const currentTreeChild = currentTreeChildren[slot]\n if (\n currentTreeChild === undefined ||\n isNavigatingToNewRootLayout(currentTreeChild, nextTreeChild)\n ) {\n return true\n }\n }\n }\n return false\n}\n"],"names":["PrefetchHint","isNavigatingToNewRootLayout","currentTree","nextTree","currentTreeSegment","nextTreeSegment","segment","Array","isArray","currentIsRootLayout","IsRootLayout","nextIsRootLayout","prefetchHints","slots","currentTreeChildren","slot","nextTreeChild","currentTreeChild","undefined"],"mappings":"AACA,SAASA,YAAY,QAAQ,uCAAsC;AAGnE,OAAO,SAASC,4BACdC,WAA8B,EAC9BC,QAAmB;IAEnB,mBAAmB;IACnB,MAAMC,qBAAqBF,WAAW,CAAC,EAAE;IACzC,MAAMG,kBAAkBF,SAASG,OAAO;IAExC,2FAA2F;IAC3F,4DAA4D;IAC5D,uIAAuI;IACvI,IAAIC,MAAMC,OAAO,CAACJ,uBAAuBG,MAAMC,OAAO,CAACH,kBAAkB;QACvE,sHAAsH;QACtH,uGAAuG;QACvG,IACED,kBAAkB,CAAC,EAAE,KAAKC,eAAe,CAAC,EAAE,IAC5CD,kBAAkB,CAAC,EAAE,KAAKC,eAAe,CAAC,EAAE,EAC5C;YACA,OAAO;QACT;IACF,OAAO,IAAID,uBAAuBC,iBAAiB;QACjD,OAAO;IACT;IAEA,iCAAiC;IACjC,MAAMI,sBACJ,AAAC,CAAA,AAACP,CAAAA,WAAW,CAAC,EAAE,IAAI,CAAA,IAAKF,aAAaU,YAAY,AAAD,MAAO;IAC1D,MAAMC,mBACJ,AAACR,CAAAA,SAASS,aAAa,GAAGZ,aAAaU,YAAY,AAAD,MAAO;IAC3D,IAAID,qBAAqB;QACvB,4EAA4E;QAC5E,OAAO,CAACE;IACV;IACA,oEAAoE;IACpE,IAAIA,kBAAkB;QACpB,OAAO;IACT;IAEA,MAAME,QAAQV,SAASU,KAAK;IAC5B,MAAMC,sBAAsBZ,WAAW,CAAC,EAAE;IAC1C,IAAIW,UAAU,MAAM;QAClB,IAAK,MAAME,QAAQF,MAAO;YACxB,MAAMG,gBAAgBH,KAAK,CAACE,KAAK;YACjC,MAAME,mBAAmBH,mBAAmB,CAACC,KAAK;YAClD,IACEE,qBAAqBC,aACrBjB,4BAA4BgB,kBAAkBD,gBAC9C;gBACA,OAAO;YACT;QACF;IACF;IACA,OAAO;AACT","ignoreList":[0]}
|
||||
+1349
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
// The tree from the last state that was committed to the browser history
|
||||
// (i.e., the last state for which HistoryUpdater's useInsertionEffect ran).
|
||||
// This lets the server-patch reducer distinguish between retrying a
|
||||
// navigation that already pushed a history entry vs one whose transition
|
||||
// suspended and never committed.
|
||||
//
|
||||
// Currently only used by the server-patch retry logic, but this module is a
|
||||
// stepping stone toward a broader refactor of the navigation queue. The
|
||||
// existing AppRouter action queue will eventually be replaced by a more
|
||||
// reactive model that explicitly tracks pending vs committed navigation
|
||||
// state. This file will likely evolve into (or be subsumed by) that new
|
||||
// implementation.
|
||||
let lastCommittedTree = null;
|
||||
export function getLastCommittedTree() {
|
||||
return lastCommittedTree;
|
||||
}
|
||||
export function setLastCommittedTree(tree) {
|
||||
lastCommittedTree = tree;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=committed-state.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/committed-state.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../../shared/lib/app-router-types'\n\n// The tree from the last state that was committed to the browser history\n// (i.e., the last state for which HistoryUpdater's useInsertionEffect ran).\n// This lets the server-patch reducer distinguish between retrying a\n// navigation that already pushed a history entry vs one whose transition\n// suspended and never committed.\n//\n// Currently only used by the server-patch retry logic, but this module is a\n// stepping stone toward a broader refactor of the navigation queue. The\n// existing AppRouter action queue will eventually be replaced by a more\n// reactive model that explicitly tracks pending vs committed navigation\n// state. This file will likely evolve into (or be subsumed by) that new\n// implementation.\nlet lastCommittedTree: FlightRouterState | null = null\n\nexport function getLastCommittedTree(): FlightRouterState | null {\n return lastCommittedTree\n}\n\nexport function setLastCommittedTree(tree: FlightRouterState): void {\n lastCommittedTree = tree\n}\n"],"names":["lastCommittedTree","getLastCommittedTree","setLastCommittedTree","tree"],"mappings":"AAEA,yEAAyE;AACzE,4EAA4E;AAC5E,oEAAoE;AACpE,yEAAyE;AACzE,iCAAiC;AACjC,EAAE;AACF,4EAA4E;AAC5E,wEAAwE;AACxE,wEAAwE;AACxE,wEAAwE;AACxE,wEAAwE;AACxE,kBAAkB;AAClB,IAAIA,oBAA8C;AAElD,OAAO,SAASC;IACd,OAAOD;AACT;AAEA,OAAO,SAASE,qBAAqBC,IAAuB;IAC1DH,oBAAoBG;AACtB","ignoreList":[0]}
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import { DEFAULT_SEGMENT_KEY } from '../../../../shared/lib/segment';
|
||||
import { createRouterCacheKey } from '../create-router-cache-key';
|
||||
export function findHeadInCache(cache, parallelRoutes) {
|
||||
return findHeadInCacheImpl(cache, parallelRoutes, '', '');
|
||||
}
|
||||
function findHeadInCacheImpl(cache, parallelRoutes, keyPrefix, keyPrefixWithoutSearchParams) {
|
||||
const isLastItem = Object.keys(parallelRoutes).length === 0;
|
||||
if (isLastItem) {
|
||||
// Returns the entire Cache Node of the segment whose head we will render.
|
||||
return [
|
||||
cache,
|
||||
keyPrefix,
|
||||
keyPrefixWithoutSearchParams
|
||||
];
|
||||
}
|
||||
// First try the 'children' parallel route if it exists
|
||||
// when starting from the "root", this corresponds with the main page component
|
||||
const parallelRoutesKeys = Object.keys(parallelRoutes).filter((key)=>key !== 'children');
|
||||
// if we are at the root, we need to check the children slot first
|
||||
if ('children' in parallelRoutes) {
|
||||
parallelRoutesKeys.unshift('children');
|
||||
}
|
||||
const slots = cache.slots;
|
||||
if (slots !== null) {
|
||||
for (const key of parallelRoutesKeys){
|
||||
const [segment, childParallelRoutes] = parallelRoutes[key];
|
||||
// If the parallel is not matched and using the default segment,
|
||||
// skip searching the head from it.
|
||||
if (segment === DEFAULT_SEGMENT_KEY) {
|
||||
continue;
|
||||
}
|
||||
const childCacheNode = slots[key];
|
||||
if (!childCacheNode) {
|
||||
continue;
|
||||
}
|
||||
const cacheKey = createRouterCacheKey(segment);
|
||||
const cacheKeyWithoutSearchParams = createRouterCacheKey(segment, true);
|
||||
const item = findHeadInCacheImpl(childCacheNode, childParallelRoutes, keyPrefix + '/' + cacheKey, keyPrefix + '/' + cacheKeyWithoutSearchParams);
|
||||
if (item) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=find-head-in-cache.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/find-head-in-cache.ts"],"sourcesContent":["import type {\n FlightRouterState,\n CacheNode,\n} from '../../../../shared/lib/app-router-types'\nimport { DEFAULT_SEGMENT_KEY } from '../../../../shared/lib/segment'\nimport { createRouterCacheKey } from '../create-router-cache-key'\n\nexport function findHeadInCache(\n cache: CacheNode,\n parallelRoutes: FlightRouterState[1]\n): [CacheNode, string, string] | null {\n return findHeadInCacheImpl(cache, parallelRoutes, '', '')\n}\n\nfunction findHeadInCacheImpl(\n cache: CacheNode,\n parallelRoutes: FlightRouterState[1],\n keyPrefix: string,\n keyPrefixWithoutSearchParams: string\n): [CacheNode, string, string] | null {\n const isLastItem = Object.keys(parallelRoutes).length === 0\n if (isLastItem) {\n // Returns the entire Cache Node of the segment whose head we will render.\n return [cache, keyPrefix, keyPrefixWithoutSearchParams]\n }\n\n // First try the 'children' parallel route if it exists\n // when starting from the \"root\", this corresponds with the main page component\n const parallelRoutesKeys = Object.keys(parallelRoutes).filter(\n (key) => key !== 'children'\n )\n\n // if we are at the root, we need to check the children slot first\n if ('children' in parallelRoutes) {\n parallelRoutesKeys.unshift('children')\n }\n\n const slots = cache.slots\n if (slots !== null) {\n for (const key of parallelRoutesKeys) {\n const [segment, childParallelRoutes] = parallelRoutes[key]\n // If the parallel is not matched and using the default segment,\n // skip searching the head from it.\n if (segment === DEFAULT_SEGMENT_KEY) {\n continue\n }\n\n const childCacheNode = slots[key]\n if (!childCacheNode) {\n continue\n }\n\n const cacheKey = createRouterCacheKey(segment)\n const cacheKeyWithoutSearchParams = createRouterCacheKey(segment, true)\n\n const item = findHeadInCacheImpl(\n childCacheNode,\n childParallelRoutes,\n keyPrefix + '/' + cacheKey,\n keyPrefix + '/' + cacheKeyWithoutSearchParams\n )\n\n if (item) {\n return item\n }\n }\n }\n\n return null\n}\n"],"names":["DEFAULT_SEGMENT_KEY","createRouterCacheKey","findHeadInCache","cache","parallelRoutes","findHeadInCacheImpl","keyPrefix","keyPrefixWithoutSearchParams","isLastItem","Object","keys","length","parallelRoutesKeys","filter","key","unshift","slots","segment","childParallelRoutes","childCacheNode","cacheKey","cacheKeyWithoutSearchParams","item"],"mappings":"AAIA,SAASA,mBAAmB,QAAQ,iCAAgC;AACpE,SAASC,oBAAoB,QAAQ,6BAA4B;AAEjE,OAAO,SAASC,gBACdC,KAAgB,EAChBC,cAAoC;IAEpC,OAAOC,oBAAoBF,OAAOC,gBAAgB,IAAI;AACxD;AAEA,SAASC,oBACPF,KAAgB,EAChBC,cAAoC,EACpCE,SAAiB,EACjBC,4BAAoC;IAEpC,MAAMC,aAAaC,OAAOC,IAAI,CAACN,gBAAgBO,MAAM,KAAK;IAC1D,IAAIH,YAAY;QACd,0EAA0E;QAC1E,OAAO;YAACL;YAAOG;YAAWC;SAA6B;IACzD;IAEA,uDAAuD;IACvD,+EAA+E;IAC/E,MAAMK,qBAAqBH,OAAOC,IAAI,CAACN,gBAAgBS,MAAM,CAC3D,CAACC,MAAQA,QAAQ;IAGnB,kEAAkE;IAClE,IAAI,cAAcV,gBAAgB;QAChCQ,mBAAmBG,OAAO,CAAC;IAC7B;IAEA,MAAMC,QAAQb,MAAMa,KAAK;IACzB,IAAIA,UAAU,MAAM;QAClB,KAAK,MAAMF,OAAOF,mBAAoB;YACpC,MAAM,CAACK,SAASC,oBAAoB,GAAGd,cAAc,CAACU,IAAI;YAC1D,gEAAgE;YAChE,mCAAmC;YACnC,IAAIG,YAAYjB,qBAAqB;gBACnC;YACF;YAEA,MAAMmB,iBAAiBH,KAAK,CAACF,IAAI;YACjC,IAAI,CAACK,gBAAgB;gBACnB;YACF;YAEA,MAAMC,WAAWnB,qBAAqBgB;YACtC,MAAMI,8BAA8BpB,qBAAqBgB,SAAS;YAElE,MAAMK,OAAOjB,oBACXc,gBACAD,qBACAZ,YAAY,MAAMc,UAClBd,YAAY,MAAMe;YAGpB,IAAIC,MAAM;gBACR,OAAOA;YACT;QACF;IACF;IAEA,OAAO;AACT","ignoreList":[0]}
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
import { isInterceptionRouteAppPath } from '../../../../shared/lib/router/utils/interception-routes';
|
||||
export function hasInterceptionRouteInCurrentTree([segment, parallelRoutes]) {
|
||||
// If we have a dynamic segment, it's marked as an interception route by the presence of the `i` suffix.
|
||||
if (Array.isArray(segment) && (segment[2] === 'di(..)(..)' || segment[2] === 'ci(..)(..)' || segment[2] === 'di(.)' || segment[2] === 'ci(.)' || segment[2] === 'di(..)' || segment[2] === 'ci(..)' || segment[2] === 'di(...)' || segment[2] === 'ci(...)')) {
|
||||
return true;
|
||||
}
|
||||
// If segment is not an array, apply the existing string-based check
|
||||
if (typeof segment === 'string' && isInterceptionRouteAppPath(segment)) {
|
||||
return true;
|
||||
}
|
||||
// Iterate through parallelRoutes if they exist
|
||||
if (parallelRoutes) {
|
||||
for(const key in parallelRoutes){
|
||||
if (hasInterceptionRouteInCurrentTree(parallelRoutes[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=has-interception-route-in-current-tree.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/has-interception-route-in-current-tree.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../../shared/lib/app-router-types'\nimport { isInterceptionRouteAppPath } from '../../../../shared/lib/router/utils/interception-routes'\n\nexport function hasInterceptionRouteInCurrentTree([\n segment,\n parallelRoutes,\n]: FlightRouterState): boolean {\n // If we have a dynamic segment, it's marked as an interception route by the presence of the `i` suffix.\n if (\n Array.isArray(segment) &&\n (segment[2] === 'di(..)(..)' ||\n segment[2] === 'ci(..)(..)' ||\n segment[2] === 'di(.)' ||\n segment[2] === 'ci(.)' ||\n segment[2] === 'di(..)' ||\n segment[2] === 'ci(..)' ||\n segment[2] === 'di(...)' ||\n segment[2] === 'ci(...)')\n ) {\n return true\n }\n\n // If segment is not an array, apply the existing string-based check\n if (typeof segment === 'string' && isInterceptionRouteAppPath(segment)) {\n return true\n }\n\n // Iterate through parallelRoutes if they exist\n if (parallelRoutes) {\n for (const key in parallelRoutes) {\n if (hasInterceptionRouteInCurrentTree(parallelRoutes[key])) {\n return true\n }\n }\n }\n\n return false\n}\n"],"names":["isInterceptionRouteAppPath","hasInterceptionRouteInCurrentTree","segment","parallelRoutes","Array","isArray","key"],"mappings":"AACA,SAASA,0BAA0B,QAAQ,0DAAyD;AAEpG,OAAO,SAASC,kCAAkC,CAChDC,SACAC,eACkB;IAClB,wGAAwG;IACxG,IACEC,MAAMC,OAAO,CAACH,YACbA,CAAAA,OAAO,CAAC,EAAE,KAAK,gBACdA,OAAO,CAAC,EAAE,KAAK,gBACfA,OAAO,CAAC,EAAE,KAAK,WACfA,OAAO,CAAC,EAAE,KAAK,WACfA,OAAO,CAAC,EAAE,KAAK,YACfA,OAAO,CAAC,EAAE,KAAK,YACfA,OAAO,CAAC,EAAE,KAAK,aACfA,OAAO,CAAC,EAAE,KAAK,SAAQ,GACzB;QACA,OAAO;IACT;IAEA,oEAAoE;IACpE,IAAI,OAAOA,YAAY,YAAYF,2BAA2BE,UAAU;QACtE,OAAO;IACT;IAEA,+CAA+C;IAC/C,IAAIC,gBAAgB;QAClB,IAAK,MAAMG,OAAOH,eAAgB;YAChC,IAAIF,kCAAkCE,cAAc,CAACG,IAAI,GAAG;gBAC1D,OAAO;YACT;QACF;IACF;IAEA,OAAO;AACT","ignoreList":[0]}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { refreshDynamicData } from './refresh-reducer';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
export function hmrRefreshReducer(state) {
|
||||
return refreshDynamicData(state, FreshnessPolicy.HMRRefresh);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=hmr-refresh-reducer.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts"],"sourcesContent":["import type {\n ReadonlyReducerState,\n ReducerState,\n} from '../router-reducer-types'\nimport { refreshDynamicData } from './refresh-reducer'\nimport { FreshnessPolicy } from '../ppr-navigations'\n\nexport function hmrRefreshReducer(state: ReadonlyReducerState): ReducerState {\n return refreshDynamicData(state, FreshnessPolicy.HMRRefresh)\n}\n"],"names":["refreshDynamicData","FreshnessPolicy","hmrRefreshReducer","state","HMRRefresh"],"mappings":"AAIA,SAASA,kBAAkB,QAAQ,oBAAmB;AACtD,SAASC,eAAe,QAAQ,qBAAoB;AAEpD,OAAO,SAASC,kBAAkBC,KAA2B;IAC3D,OAAOH,mBAAmBG,OAAOF,gBAAgBG,UAAU;AAC7D","ignoreList":[0]}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
import { completeHardNavigation, navigate as navigateUsingSegmentCache } from '../../segment-cache/navigation';
|
||||
import { getStaleTimeMs } from '../../segment-cache/cache';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
// These values are set by `define-env-plugin` (based on `nextConfig.experimental.staleTimes`)
|
||||
// and default to 5 minutes (static) / 0 seconds (dynamic)
|
||||
export const DYNAMIC_STALETIME_MS = Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000;
|
||||
export const STATIC_STALETIME_MS = getStaleTimeMs(Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME));
|
||||
export function navigateReducer(state, action) {
|
||||
const { url, isExternalUrl, navigateType, scrollBehavior } = action;
|
||||
if (isExternalUrl) {
|
||||
return completeHardNavigation(state, url, navigateType);
|
||||
}
|
||||
// Handles case where `<meta http-equiv="refresh">` tag is present,
|
||||
// which will trigger an MPA navigation.
|
||||
if (document.getElementById('__next-page-redirect')) {
|
||||
return completeHardNavigation(state, url, navigateType);
|
||||
}
|
||||
// Temporary glue code between the router reducer and the new navigation
|
||||
// implementation. Eventually we'll rewrite the router reducer to a
|
||||
// state machine.
|
||||
const currentUrl = new URL(state.canonicalUrl, location.origin);
|
||||
const currentRenderedSearch = state.renderedSearch;
|
||||
return navigateUsingSegmentCache(state, url, currentUrl, currentRenderedSearch, state.cache, state.tree, state.nextUrl, FreshnessPolicy.Default, scrollBehavior, navigateType);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=navigate-reducer.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/navigate-reducer.ts"],"sourcesContent":["import type {\n NavigateAction,\n ReadonlyReducerState,\n ReducerState,\n} from '../router-reducer-types'\n\nimport {\n completeHardNavigation,\n navigate as navigateUsingSegmentCache,\n} from '../../segment-cache/navigation'\nimport { getStaleTimeMs } from '../../segment-cache/cache'\nimport { FreshnessPolicy } from '../ppr-navigations'\n\n// These values are set by `define-env-plugin` (based on `nextConfig.experimental.staleTimes`)\n// and default to 5 minutes (static) / 0 seconds (dynamic)\nexport const DYNAMIC_STALETIME_MS =\n Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000\n\nexport const STATIC_STALETIME_MS = getStaleTimeMs(\n Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME)\n)\n\nexport function navigateReducer(\n state: ReadonlyReducerState,\n action: NavigateAction\n): ReducerState {\n const { url, isExternalUrl, navigateType, scrollBehavior } = action\n\n if (isExternalUrl) {\n return completeHardNavigation(state, url, navigateType)\n }\n\n // Handles case where `<meta http-equiv=\"refresh\">` tag is present,\n // which will trigger an MPA navigation.\n if (document.getElementById('__next-page-redirect')) {\n return completeHardNavigation(state, url, navigateType)\n }\n\n // Temporary glue code between the router reducer and the new navigation\n // implementation. Eventually we'll rewrite the router reducer to a\n // state machine.\n const currentUrl = new URL(state.canonicalUrl, location.origin)\n const currentRenderedSearch = state.renderedSearch\n return navigateUsingSegmentCache(\n state,\n url,\n currentUrl,\n currentRenderedSearch,\n state.cache,\n state.tree,\n state.nextUrl,\n FreshnessPolicy.Default,\n scrollBehavior,\n navigateType\n )\n}\n"],"names":["completeHardNavigation","navigate","navigateUsingSegmentCache","getStaleTimeMs","FreshnessPolicy","DYNAMIC_STALETIME_MS","Number","process","env","__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME","STATIC_STALETIME_MS","__NEXT_CLIENT_ROUTER_STATIC_STALETIME","navigateReducer","state","action","url","isExternalUrl","navigateType","scrollBehavior","document","getElementById","currentUrl","URL","canonicalUrl","location","origin","currentRenderedSearch","renderedSearch","cache","tree","nextUrl","Default"],"mappings":"AAMA,SACEA,sBAAsB,EACtBC,YAAYC,yBAAyB,QAChC,iCAAgC;AACvC,SAASC,cAAc,QAAQ,4BAA2B;AAC1D,SAASC,eAAe,QAAQ,qBAAoB;AAEpD,8FAA8F;AAC9F,0DAA0D;AAC1D,OAAO,MAAMC,uBACXC,OAAOC,QAAQC,GAAG,CAACC,sCAAsC,IAAI,KAAI;AAEnE,OAAO,MAAMC,sBAAsBP,eACjCG,OAAOC,QAAQC,GAAG,CAACG,qCAAqC,GACzD;AAED,OAAO,SAASC,gBACdC,KAA2B,EAC3BC,MAAsB;IAEtB,MAAM,EAAEC,GAAG,EAAEC,aAAa,EAAEC,YAAY,EAAEC,cAAc,EAAE,GAAGJ;IAE7D,IAAIE,eAAe;QACjB,OAAOhB,uBAAuBa,OAAOE,KAAKE;IAC5C;IAEA,mEAAmE;IACnE,wCAAwC;IACxC,IAAIE,SAASC,cAAc,CAAC,yBAAyB;QACnD,OAAOpB,uBAAuBa,OAAOE,KAAKE;IAC5C;IAEA,wEAAwE;IACxE,mEAAmE;IACnE,iBAAiB;IACjB,MAAMI,aAAa,IAAIC,IAAIT,MAAMU,YAAY,EAAEC,SAASC,MAAM;IAC9D,MAAMC,wBAAwBb,MAAMc,cAAc;IAClD,OAAOzB,0BACLW,OACAE,KACAM,YACAK,uBACAb,MAAMe,KAAK,EACXf,MAAMgB,IAAI,EACVhB,MAAMiB,OAAO,EACb1B,gBAAgB2B,OAAO,EACvBb,gBACAD;AAEJ","ignoreList":[0]}
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
import { ScrollBehavior } from '../router-reducer-types';
|
||||
import { convertServerPatchToFullTree, navigateToKnownRoute } from '../../segment-cache/navigation';
|
||||
import { invalidateSegmentCacheEntries } from '../../segment-cache/cache';
|
||||
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
import { invalidateBfCache, UnknownDynamicStaleTime } from '../../segment-cache/bfcache';
|
||||
export function refreshReducer(state, action) {
|
||||
// During a refresh, we invalidate the segment cache but not the route cache.
|
||||
// The route cache contains the tree structure (which segments exist at a
|
||||
// given URL) which doesn't change during a refresh. The segment cache
|
||||
// contains the actual RSC data which needs to be re-fetched.
|
||||
//
|
||||
// The Instant Navigation Testing API can bypass cache invalidation to
|
||||
// preserve prefetched data when refreshing after an MPA navigation. This is
|
||||
// only used for testing and is not exposed in production builds by default.
|
||||
const bypassCacheInvalidation = process.env.__NEXT_EXPOSE_TESTING_API && action.bypassCacheInvalidation;
|
||||
if (!bypassCacheInvalidation) {
|
||||
const currentNextUrl = state.nextUrl;
|
||||
const currentRouterState = state.tree;
|
||||
invalidateSegmentCacheEntries(currentNextUrl, currentRouterState);
|
||||
}
|
||||
return refreshDynamicData(state, FreshnessPolicy.RefreshAll);
|
||||
}
|
||||
export function refreshDynamicData(state, freshnessPolicy) {
|
||||
// During a refresh, invalidate the BFCache, which may contain dynamic data.
|
||||
invalidateBfCache();
|
||||
const currentNextUrl = state.nextUrl;
|
||||
// We always send the last next-url, not the current when performing a dynamic
|
||||
// request. This is because we update the next-url after a navigation, but we
|
||||
// want the same interception route to be matched that used the last next-url.
|
||||
const nextUrlForRefresh = hasInterceptionRouteInCurrentTree(state.tree) ? state.previousNextUrl || currentNextUrl : null;
|
||||
// A refresh is modeled as a navigation to the current URL, but where any
|
||||
// existing dynamic data (including in shared layouts) is re-fetched.
|
||||
const currentCanonicalUrl = state.canonicalUrl;
|
||||
const currentUrl = new URL(currentCanonicalUrl, location.origin);
|
||||
const currentRenderedSearch = state.renderedSearch;
|
||||
const currentFlightRouterState = state.tree;
|
||||
const scrollBehavior = ScrollBehavior.NoScroll;
|
||||
// Create a NavigationSeed from the current FlightRouterState.
|
||||
// TODO: Eventually we will store this type directly on the state object
|
||||
// instead of reconstructing it on demand. Part of a larger series of
|
||||
// refactors to unify the various tree types that the client deals with.
|
||||
const now = Date.now();
|
||||
// TODO: Store the dynamic stale time on the top-level state so it's known
|
||||
// during restores and refreshes.
|
||||
const refreshSeed = convertServerPatchToFullTree(now, currentFlightRouterState, null, currentRenderedSearch, UnknownDynamicStaleTime);
|
||||
const navigateType = 'replace';
|
||||
return navigateToKnownRoute(now, state, currentUrl, currentCanonicalUrl, refreshSeed, currentUrl, currentRenderedSearch, state.cache, currentFlightRouterState, freshnessPolicy, nextUrlForRefresh, scrollBehavior, navigateType, null, // Refresh navigations don't use route prediction, so there's no route
|
||||
// cache entry to mark as having a dynamic rewrite on mismatch. If a
|
||||
// mismatch occurs, the retry handler will traverse the known route tree
|
||||
// to find and mark the entry.
|
||||
null);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=refresh-reducer.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
import { extractPathFromFlightRouterState } from '../compute-changed-path';
|
||||
import { FreshnessPolicy, spawnDynamicRequests, startPPRNavigation } from '../ppr-navigations';
|
||||
import { completeHardNavigation, completeTraverseNavigation, convertServerPatchToFullTree } from '../../segment-cache/navigation';
|
||||
import { UnknownDynamicStaleTime } from '../../segment-cache/bfcache';
|
||||
export function restoreReducer(state, action) {
|
||||
// This action is used to restore the router state from the history state.
|
||||
// However, it's possible that the history state no longer contains the `FlightRouterState`.
|
||||
// We will copy over the internal state on pushState/replaceState events, but if a history entry
|
||||
// occurred before hydration, or if the user navigated to a hash using a regular anchor link,
|
||||
// the history state will not contain the `FlightRouterState`.
|
||||
// In this case, we'll continue to use the existing tree so the router doesn't get into an invalid state.
|
||||
let treeToRestore;
|
||||
let renderedSearch;
|
||||
const historyState = action.historyState;
|
||||
if (historyState) {
|
||||
treeToRestore = historyState.tree;
|
||||
renderedSearch = historyState.renderedSearch;
|
||||
} else {
|
||||
treeToRestore = state.tree;
|
||||
renderedSearch = state.renderedSearch;
|
||||
}
|
||||
const currentUrl = new URL(state.canonicalUrl, location.origin);
|
||||
const restoredUrl = action.url;
|
||||
const restoredNextUrl = extractPathFromFlightRouterState(treeToRestore) ?? restoredUrl.pathname;
|
||||
const now = Date.now();
|
||||
// TODO: Store the dynamic stale time on the top-level state so it's known
|
||||
// during restores and refreshes.
|
||||
const accumulation = {
|
||||
separateRefreshUrls: null,
|
||||
scrollRef: null
|
||||
};
|
||||
const restoreSeed = convertServerPatchToFullTree(now, treeToRestore, null, renderedSearch, UnknownDynamicStaleTime);
|
||||
const task = startPPRNavigation(now, currentUrl, state.renderedSearch, state.cache, state.tree, restoreSeed.routeTree, restoreSeed.metadataVaryPath, FreshnessPolicy.HistoryTraversal, null, null, restoreSeed.dynamicStaleAt, false, accumulation);
|
||||
if (task === null) {
|
||||
return completeHardNavigation(state, restoredUrl, 'replace');
|
||||
}
|
||||
spawnDynamicRequests(task, restoredUrl, restoredNextUrl, FreshnessPolicy.HistoryTraversal, accumulation, // History traversal doesn't use route prediction, so there's no route
|
||||
// cache entry to mark as having a dynamic rewrite on mismatch. If a
|
||||
// mismatch occurs, the retry handler will traverse the known route tree
|
||||
// to find and mark the entry.
|
||||
null, // History traversal always uses 'replace'.
|
||||
'replace');
|
||||
return completeTraverseNavigation(state, restoredUrl, renderedSearch, task.node, task.route, restoredNextUrl);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=restore-reducer.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+309
@@ -0,0 +1,309 @@
|
||||
import { callServer } from '../../../app-call-server';
|
||||
import { findSourceMapURL } from '../../../app-find-source-map-url';
|
||||
import { ACTION_HEADER, NEXT_ACTION_NOT_FOUND_HEADER, NEXT_IS_PRERENDER_HEADER, NEXT_HTML_REQUEST_ID_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL, RSC_CONTENT_TYPE_HEADER, NEXT_REQUEST_ID_HEADER } from '../../app-router-headers';
|
||||
import { UnrecognizedActionError } from '../../unrecognized-action-error';
|
||||
// TODO: Explicitly import from client.browser
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { createFromFetch as createFromFetchBrowser, createTemporaryReferenceSet, encodeReply } from 'react-server-dom-webpack/client';
|
||||
import { ScrollBehavior } from '../router-reducer-types';
|
||||
import { assignLocation } from '../../../assign-location';
|
||||
import { createHrefFromUrl } from '../create-href-from-url';
|
||||
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree';
|
||||
import { normalizeFlightData, prepareFlightRouterStateForRequest } from '../../../flight-data-helpers';
|
||||
import { getRedirectError } from '../../redirect';
|
||||
import { removeBasePath } from '../../../remove-base-path';
|
||||
import { hasBasePath } from '../../../has-base-path';
|
||||
import { extractInfoFromServerReferenceId, omitUnusedArgs } from '../../../../shared/lib/server-reference-info';
|
||||
import { invalidateEntirePrefetchCache } from '../../segment-cache/cache';
|
||||
import { startRevalidationCooldown } from '../../segment-cache/scheduler';
|
||||
import { getDeploymentId } from '../../../../shared/lib/deployment-id';
|
||||
import { getNavigationBuildId } from '../../../navigation-build-id';
|
||||
import { NEXT_NAV_DEPLOYMENT_ID_HEADER } from '../../../../lib/constants';
|
||||
import { completeHardNavigation, convertServerPatchToFullTree, navigateToKnownRoute, navigate } from '../../segment-cache/navigation';
|
||||
import { discoverKnownRoute } from '../../segment-cache/optimistic-routes';
|
||||
import { ActionDidNotRevalidate, ActionDidRevalidateDynamicOnly, ActionDidRevalidateStaticAndDynamic } from '../../../../shared/lib/action-revalidation-kind';
|
||||
import { isExternalURL } from '../../app-router-utils';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
import { processFetch } from '../fetch-server-response';
|
||||
import { invalidateBfCache, UnknownDynamicStaleTime } from '../../segment-cache/bfcache';
|
||||
const createFromFetch = createFromFetchBrowser;
|
||||
let createDebugChannel;
|
||||
if (process.env.__NEXT_DEV_SERVER && process.env.__NEXT_REACT_DEBUG_CHANNEL) {
|
||||
createDebugChannel = require('../../../dev/debug-channel').createDebugChannel;
|
||||
}
|
||||
async function fetchServerAction(state, nextUrl, { actionId, actionArgs }) {
|
||||
const temporaryReferences = createTemporaryReferenceSet();
|
||||
const info = extractInfoFromServerReferenceId(actionId);
|
||||
const usedArgs = omitUnusedArgs(actionArgs, info);
|
||||
const body = await encodeReply(usedArgs, {
|
||||
temporaryReferences
|
||||
});
|
||||
const headers = {
|
||||
Accept: RSC_CONTENT_TYPE_HEADER,
|
||||
[ACTION_HEADER]: actionId,
|
||||
[NEXT_ROUTER_STATE_TREE_HEADER]: prepareFlightRouterStateForRequest(state.tree)
|
||||
};
|
||||
const deploymentId = getDeploymentId();
|
||||
if (deploymentId) {
|
||||
headers['x-deployment-id'] = deploymentId;
|
||||
}
|
||||
if (nextUrl) {
|
||||
headers[NEXT_URL] = nextUrl;
|
||||
}
|
||||
if (process.env.__NEXT_DEV_SERVER) {
|
||||
if (self.__next_r) {
|
||||
headers[NEXT_HTML_REQUEST_ID_HEADER] = self.__next_r;
|
||||
}
|
||||
// Create a new request ID for the server action request. The server uses
|
||||
// this to tag debug information sent via WebSocket to the client, which
|
||||
// then routes those chunks to the debug channel associated with this ID.
|
||||
headers[NEXT_REQUEST_ID_HEADER] = crypto.getRandomValues(new Uint32Array(1))[0].toString(16);
|
||||
}
|
||||
const res = await fetch(state.canonicalUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
});
|
||||
// Handle server actions that the server didn't recognize.
|
||||
const unrecognizedActionHeader = res.headers.get(NEXT_ACTION_NOT_FOUND_HEADER);
|
||||
if (unrecognizedActionHeader === '1') {
|
||||
throw Object.defineProperty(new UnrecognizedActionError(`Server Action "${actionId}" was not found on the server. \nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", {
|
||||
value: "E715",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
const redirectHeader = res.headers.get('x-action-redirect');
|
||||
const [location1, _redirectType] = redirectHeader?.split(';') || [];
|
||||
let redirectType;
|
||||
switch(_redirectType){
|
||||
case 'push':
|
||||
redirectType = 'push';
|
||||
break;
|
||||
case 'replace':
|
||||
redirectType = 'replace';
|
||||
break;
|
||||
default:
|
||||
redirectType = undefined;
|
||||
}
|
||||
const isPrerender = !!res.headers.get(NEXT_IS_PRERENDER_HEADER);
|
||||
let revalidationKind = ActionDidNotRevalidate;
|
||||
try {
|
||||
const revalidationHeader = res.headers.get('x-action-revalidated');
|
||||
if (revalidationHeader) {
|
||||
const parsedKind = JSON.parse(revalidationHeader);
|
||||
if (parsedKind === ActionDidRevalidateStaticAndDynamic || parsedKind === ActionDidRevalidateDynamicOnly) {
|
||||
revalidationKind = parsedKind;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
const redirectLocation = location1 ? assignLocation(location1, new URL(state.canonicalUrl, window.location.href)) : undefined;
|
||||
const contentType = res.headers.get('content-type');
|
||||
const isRscResponse = !!(contentType && contentType.startsWith(RSC_CONTENT_TYPE_HEADER));
|
||||
// Handle invalid server action responses.
|
||||
// A valid response must have `content-type: text/x-component`, unless it's an external redirect.
|
||||
// (external redirects have an 'x-action-redirect' header, but the body is an empty 'text/plain')
|
||||
if (!isRscResponse && !redirectLocation) {
|
||||
// The server can respond with a text/plain error message, but we'll fallback to something generic
|
||||
// if there isn't one.
|
||||
const message = res.status >= 400 && contentType === 'text/plain' ? await res.text() : 'An unexpected response was received from the server.';
|
||||
throw Object.defineProperty(new Error(message), "__NEXT_ERROR_CODE", {
|
||||
value: "E394",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
let actionResult;
|
||||
let actionFlightData;
|
||||
let actionFlightDataRenderedSearch;
|
||||
let couldBeIntercepted = false;
|
||||
if (isRscResponse) {
|
||||
// Server action redirect responses carry the Flight data of the redirect
|
||||
// target, which may be prerendered with a completeness marker byte
|
||||
// prepended. Strip it before passing to Flight.
|
||||
const responsePromise = redirectLocation ? processFetch(res).then(({ response: r })=>r) : Promise.resolve(res);
|
||||
const response = await createFromFetch(responsePromise, {
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
temporaryReferences,
|
||||
debugChannel: createDebugChannel && createDebugChannel(headers)
|
||||
});
|
||||
// An internal redirect can send an RSC response, but does not have a useful `actionResult`.
|
||||
actionResult = redirectLocation ? undefined : response.a;
|
||||
couldBeIntercepted = response.i;
|
||||
// Check if the response build ID matches the client build ID.
|
||||
// In a multi-zone setup, when a server action triggers a redirect,
|
||||
// the server pre-fetches the redirect target as RSC. If the redirect
|
||||
// target is served by a different Next.js zone (different build), the
|
||||
// pre-fetched RSC data will have a foreign build ID. We must discard
|
||||
// the flight data in that case so the redirect triggers an MPA
|
||||
// navigation (full page load) instead of trying to apply the foreign
|
||||
// RSC payload — which would result in a blank page.
|
||||
const responseBuildId = res.headers.get(NEXT_NAV_DEPLOYMENT_ID_HEADER) ?? response.b;
|
||||
if (responseBuildId !== undefined && responseBuildId !== getNavigationBuildId()) {
|
||||
// Build ID mismatch — discard the flight data. The redirect will
|
||||
// still be processed, and the absence of flight data will cause an
|
||||
// MPA navigation via completeHardNavigation().
|
||||
} else {
|
||||
const maybeFlightData = normalizeFlightData(response.f);
|
||||
if (maybeFlightData !== '') {
|
||||
actionFlightData = maybeFlightData;
|
||||
actionFlightDataRenderedSearch = response.q;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// An external redirect doesn't contain RSC data.
|
||||
actionResult = undefined;
|
||||
actionFlightData = undefined;
|
||||
actionFlightDataRenderedSearch = undefined;
|
||||
}
|
||||
return {
|
||||
actionResult,
|
||||
actionFlightData,
|
||||
actionFlightDataRenderedSearch,
|
||||
redirectLocation,
|
||||
redirectType,
|
||||
revalidationKind,
|
||||
isPrerender,
|
||||
couldBeIntercepted
|
||||
};
|
||||
}
|
||||
/*
|
||||
* This reducer is responsible for calling the server action and processing any side-effects from the server action.
|
||||
* It does not mutate the state by itself but rather delegates to other reducers to do the actual mutation.
|
||||
*/ export function serverActionReducer(state, action) {
|
||||
const { resolve, reject } = action;
|
||||
// only pass along the `nextUrl` param (used for interception routes) if the current route was intercepted.
|
||||
// If the route has been intercepted, the action should be as well.
|
||||
// Otherwise the server action might be intercepted with the wrong action id
|
||||
// (ie, one that corresponds with the intercepted route)
|
||||
const nextUrl = // We always send the last next-url, not the current when
|
||||
// performing a dynamic request. This is because we update
|
||||
// the next-url after a navigation, but we want the same
|
||||
// interception route to be matched that used the last
|
||||
// next-url.
|
||||
(state.previousNextUrl || state.nextUrl) && hasInterceptionRouteInCurrentTree(state.tree) ? state.previousNextUrl || state.nextUrl : null;
|
||||
return fetchServerAction(state, nextUrl, action).then(async ({ revalidationKind, actionResult, actionFlightData: flightData, actionFlightDataRenderedSearch: flightDataRenderedSearch, redirectLocation, redirectType, isPrerender, couldBeIntercepted })=>{
|
||||
if (revalidationKind !== ActionDidNotRevalidate) {
|
||||
// There was either a revalidation or a refresh, or maybe both.
|
||||
// Evict the BFCache, which may contain dynamic data.
|
||||
invalidateBfCache();
|
||||
// Store whether this action triggered any revalidation
|
||||
// The action queue will use this information to potentially
|
||||
// trigger a refresh action if the action was discarded
|
||||
// (ie, due to a navigation, before the action completed)
|
||||
action.didRevalidate = true;
|
||||
// If there was a revalidation, evict the prefetch cache.
|
||||
// TODO: Evict only segments with matching tags and/or paths.
|
||||
// TODO: We should only invalidate the route cache if cookies were
|
||||
// mutated, since route trees may vary based on cookies. For now we
|
||||
// invalidate both caches until we have a way to detect cookie
|
||||
// mutations on the client.
|
||||
if (revalidationKind === ActionDidRevalidateStaticAndDynamic) {
|
||||
invalidateEntirePrefetchCache(nextUrl, state.tree);
|
||||
}
|
||||
// Start a cooldown before re-prefetching to allow CDN cache
|
||||
// propagation.
|
||||
startRevalidationCooldown();
|
||||
}
|
||||
const navigateType = redirectType || 'push';
|
||||
if (redirectLocation !== undefined) {
|
||||
// If the action triggered a redirect, the action promise will be rejected with
|
||||
// a redirect so that it's handled by RedirectBoundary as we won't have a valid
|
||||
// action result to resolve the promise with. This will effectively reset the state of
|
||||
// the component that called the action as the error boundary will remount the tree.
|
||||
// The status code doesn't matter here as the action handler will have already sent
|
||||
// a response with the correct status code.
|
||||
if (isExternalURL(redirectLocation)) {
|
||||
// External redirect. Triggers an MPA navigation.
|
||||
const redirectHref = redirectLocation.href;
|
||||
const redirectError = createRedirectErrorForAction(redirectHref, navigateType);
|
||||
reject(redirectError);
|
||||
return completeHardNavigation(state, redirectLocation, navigateType);
|
||||
} else {
|
||||
// Internal redirect. Triggers an SPA navigation.
|
||||
const redirectWithBasepath = createHrefFromUrl(redirectLocation, false);
|
||||
const redirectHref = hasBasePath(redirectWithBasepath) ? removeBasePath(redirectWithBasepath) : redirectWithBasepath;
|
||||
const redirectError = createRedirectErrorForAction(redirectHref, navigateType);
|
||||
reject(redirectError);
|
||||
}
|
||||
} else {
|
||||
// If there's no redirect, resolve the action with the result.
|
||||
resolve(actionResult);
|
||||
}
|
||||
// Check if we can bail out without updating any state.
|
||||
if (// Did the action trigger a redirect?
|
||||
redirectLocation === undefined && // Did the action revalidate any data?
|
||||
revalidationKind === ActionDidNotRevalidate && // Did the server render new data?
|
||||
flightData === undefined) {
|
||||
// The action did not trigger any revalidations or redirects. No
|
||||
// navigation is required.
|
||||
return state;
|
||||
}
|
||||
if (flightData === undefined && redirectLocation !== undefined) {
|
||||
// The server redirected, but did not send any Flight data. This implies
|
||||
// an external redirect.
|
||||
// TODO: We should refactor the action response type to be more explicit
|
||||
// about the various response types.
|
||||
return completeHardNavigation(state, redirectLocation, navigateType);
|
||||
}
|
||||
if (typeof flightData === 'string') {
|
||||
// If the flight data is just a string, something earlier in the
|
||||
// response handling triggered an external redirect.
|
||||
return completeHardNavigation(state, new URL(flightData, location.origin), navigateType);
|
||||
}
|
||||
// The action triggered a navigation — either a redirect, a revalidation,
|
||||
// or both.
|
||||
// If there was no redirect, then the target URL is the same as the
|
||||
// current URL.
|
||||
const currentUrl = new URL(state.canonicalUrl, location.origin);
|
||||
const currentRenderedSearch = state.renderedSearch;
|
||||
const redirectUrl = redirectLocation !== undefined ? redirectLocation : currentUrl;
|
||||
const currentFlightRouterState = state.tree;
|
||||
const scrollBehavior = ScrollBehavior.Default;
|
||||
// If the action triggered a revalidation of the cache, we should also
|
||||
// refresh all the dynamic data.
|
||||
const freshnessPolicy = revalidationKind === ActionDidNotRevalidate ? FreshnessPolicy.Default : FreshnessPolicy.RefreshAll;
|
||||
// The server may have sent back new data. If so, we will perform a
|
||||
// "seeded" navigation that uses the data from the response.
|
||||
// TODO: Currently the server always renders from the root in
|
||||
// response to a Server Action. In the case of a normal redirect
|
||||
// with no revalidation, it should skip over the shared layouts.
|
||||
if (flightData !== undefined && flightDataRenderedSearch !== undefined) {
|
||||
// The server sent back new route data as part of the response. We
|
||||
// will use this to render the new page. If this happens to be only a
|
||||
// subset of the data needed to render the new page, we'll initiate a
|
||||
// new fetch, like we would for a normal navigation.
|
||||
const redirectCanonicalUrl = createHrefFromUrl(redirectUrl);
|
||||
const now = Date.now();
|
||||
// TODO: Store the dynamic stale time on the top-level state so it's
|
||||
// known during restores and refreshes.
|
||||
const redirectSeed = convertServerPatchToFullTree(now, currentFlightRouterState, flightData, flightDataRenderedSearch, UnknownDynamicStaleTime);
|
||||
// Learn the route pattern so we can predict it for future navigations.
|
||||
const metadataVaryPath = redirectSeed.metadataVaryPath;
|
||||
if (metadataVaryPath !== null) {
|
||||
discoverKnownRoute(now, redirectUrl.pathname, nextUrl, null, redirectSeed.routeTree, metadataVaryPath, couldBeIntercepted, redirectCanonicalUrl, isPrerender, false // hasDynamicRewrite
|
||||
);
|
||||
}
|
||||
return navigateToKnownRoute(now, state, redirectUrl, redirectCanonicalUrl, redirectSeed, currentUrl, currentRenderedSearch, state.cache, currentFlightRouterState, freshnessPolicy, nextUrl, scrollBehavior, navigateType, null, // Server action redirects don't use route prediction - we already
|
||||
// have the route tree from the server response. If a mismatch occurs
|
||||
// during dynamic data fetch, the retry handler will traverse the
|
||||
// known route tree to mark the entry as having a dynamic rewrite.
|
||||
null);
|
||||
}
|
||||
// The server did not send back new data. We'll perform a regular, non-
|
||||
// seeded navigation — effectively the same as <Link> or router.push().
|
||||
return navigate(state, redirectUrl, currentUrl, currentRenderedSearch, state.cache, currentFlightRouterState, nextUrl, freshnessPolicy, scrollBehavior, navigateType);
|
||||
}, (e)=>{
|
||||
// When the server action is rejected we don't update the state and instead call the reject handler of the promise.
|
||||
reject(e);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
function createRedirectErrorForAction(redirectHref, resolvedRedirectType) {
|
||||
const redirectError = getRedirectError(redirectHref, resolvedRedirectType);
|
||||
redirectError.handled = true;
|
||||
return redirectError;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=server-action-reducer.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
import { createHrefFromUrl } from '../create-href-from-url';
|
||||
import { ACTION_REFRESH, ScrollBehavior } from '../router-reducer-types';
|
||||
import { completeHardNavigation, navigateToKnownRoute } from '../../segment-cache/navigation';
|
||||
import { refreshReducer } from './refresh-reducer';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
export function serverPatchReducer(state, action) {
|
||||
// A "retry" is a navigation that happens due to a route mismatch. It's
|
||||
// similar to a refresh, because we will omit any existing dynamic data on
|
||||
// the page. But we seed the retry navigation with the exact tree that the
|
||||
// server just responded with.
|
||||
const retryMpa = action.mpa;
|
||||
const retryUrl = new URL(action.url, location.origin);
|
||||
const retrySeed = action.seed;
|
||||
const navigateType = action.navigateType;
|
||||
if (retryMpa || retrySeed === null) {
|
||||
// If the server did not send back data during the mismatch, fall back to
|
||||
// an MPA navigation.
|
||||
return completeHardNavigation(state, retryUrl, navigateType);
|
||||
}
|
||||
const currentUrl = new URL(state.canonicalUrl, location.origin);
|
||||
const currentRenderedSearch = state.renderedSearch;
|
||||
if (action.previousTree !== state.tree) {
|
||||
// There was another, more recent navigation since the once that
|
||||
// mismatched. We can abort the retry, but we still need to refresh the
|
||||
// page to evict any stale dynamic data.
|
||||
return refreshReducer(state, {
|
||||
type: ACTION_REFRESH
|
||||
});
|
||||
}
|
||||
// There have been no new navigations since the mismatched one. Refresh,
|
||||
// using the tree we just received from the server.
|
||||
const retryCanonicalUrl = createHrefFromUrl(retryUrl);
|
||||
const retryNextUrl = action.nextUrl;
|
||||
const scrollBehavior = ScrollBehavior.Default;
|
||||
const now = Date.now();
|
||||
return navigateToKnownRoute(now, state, retryUrl, retryCanonicalUrl, retrySeed, currentUrl, currentRenderedSearch, state.cache, state.tree, FreshnessPolicy.RefreshAll, retryNextUrl, scrollBehavior, navigateType, null, // Server patch (retry) navigations don't use route prediction. This is
|
||||
// typically a retry after a previous mismatch, so the route was already
|
||||
// marked as having a dynamic rewrite when the mismatch was detected.
|
||||
null);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=server-patch-reducer.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/server-patch-reducer.ts"],"sourcesContent":["import { createHrefFromUrl } from '../create-href-from-url'\nimport {\n ACTION_REFRESH,\n type ServerPatchAction,\n type ReducerState,\n type ReadonlyReducerState,\n ScrollBehavior,\n} from '../router-reducer-types'\nimport {\n completeHardNavigation,\n navigateToKnownRoute,\n} from '../../segment-cache/navigation'\nimport { refreshReducer } from './refresh-reducer'\nimport { FreshnessPolicy } from '../ppr-navigations'\n\nexport function serverPatchReducer(\n state: ReadonlyReducerState,\n action: ServerPatchAction\n): ReducerState {\n // A \"retry\" is a navigation that happens due to a route mismatch. It's\n // similar to a refresh, because we will omit any existing dynamic data on\n // the page. But we seed the retry navigation with the exact tree that the\n // server just responded with.\n const retryMpa = action.mpa\n const retryUrl = new URL(action.url, location.origin)\n const retrySeed = action.seed\n const navigateType = action.navigateType\n if (retryMpa || retrySeed === null) {\n // If the server did not send back data during the mismatch, fall back to\n // an MPA navigation.\n return completeHardNavigation(state, retryUrl, navigateType)\n }\n const currentUrl = new URL(state.canonicalUrl, location.origin)\n const currentRenderedSearch = state.renderedSearch\n if (action.previousTree !== state.tree) {\n // There was another, more recent navigation since the once that\n // mismatched. We can abort the retry, but we still need to refresh the\n // page to evict any stale dynamic data.\n return refreshReducer(state, { type: ACTION_REFRESH })\n }\n // There have been no new navigations since the mismatched one. Refresh,\n // using the tree we just received from the server.\n const retryCanonicalUrl = createHrefFromUrl(retryUrl)\n const retryNextUrl = action.nextUrl\n const scrollBehavior = ScrollBehavior.Default\n const now = Date.now()\n return navigateToKnownRoute(\n now,\n state,\n retryUrl,\n retryCanonicalUrl,\n retrySeed,\n currentUrl,\n currentRenderedSearch,\n state.cache,\n state.tree,\n FreshnessPolicy.RefreshAll,\n retryNextUrl,\n scrollBehavior,\n navigateType,\n null,\n // Server patch (retry) navigations don't use route prediction. This is\n // typically a retry after a previous mismatch, so the route was already\n // marked as having a dynamic rewrite when the mismatch was detected.\n null\n )\n}\n"],"names":["createHrefFromUrl","ACTION_REFRESH","ScrollBehavior","completeHardNavigation","navigateToKnownRoute","refreshReducer","FreshnessPolicy","serverPatchReducer","state","action","retryMpa","mpa","retryUrl","URL","url","location","origin","retrySeed","seed","navigateType","currentUrl","canonicalUrl","currentRenderedSearch","renderedSearch","previousTree","tree","type","retryCanonicalUrl","retryNextUrl","nextUrl","scrollBehavior","Default","now","Date","cache","RefreshAll"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,0BAAyB;AAC3D,SACEC,cAAc,EAIdC,cAAc,QACT,0BAAyB;AAChC,SACEC,sBAAsB,EACtBC,oBAAoB,QACf,iCAAgC;AACvC,SAASC,cAAc,QAAQ,oBAAmB;AAClD,SAASC,eAAe,QAAQ,qBAAoB;AAEpD,OAAO,SAASC,mBACdC,KAA2B,EAC3BC,MAAyB;IAEzB,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,8BAA8B;IAC9B,MAAMC,WAAWD,OAAOE,GAAG;IAC3B,MAAMC,WAAW,IAAIC,IAAIJ,OAAOK,GAAG,EAAEC,SAASC,MAAM;IACpD,MAAMC,YAAYR,OAAOS,IAAI;IAC7B,MAAMC,eAAeV,OAAOU,YAAY;IACxC,IAAIT,YAAYO,cAAc,MAAM;QAClC,yEAAyE;QACzE,qBAAqB;QACrB,OAAOd,uBAAuBK,OAAOI,UAAUO;IACjD;IACA,MAAMC,aAAa,IAAIP,IAAIL,MAAMa,YAAY,EAAEN,SAASC,MAAM;IAC9D,MAAMM,wBAAwBd,MAAMe,cAAc;IAClD,IAAId,OAAOe,YAAY,KAAKhB,MAAMiB,IAAI,EAAE;QACtC,gEAAgE;QAChE,uEAAuE;QACvE,wCAAwC;QACxC,OAAOpB,eAAeG,OAAO;YAAEkB,MAAMzB;QAAe;IACtD;IACA,wEAAwE;IACxE,mDAAmD;IACnD,MAAM0B,oBAAoB3B,kBAAkBY;IAC5C,MAAMgB,eAAenB,OAAOoB,OAAO;IACnC,MAAMC,iBAAiB5B,eAAe6B,OAAO;IAC7C,MAAMC,MAAMC,KAAKD,GAAG;IACpB,OAAO5B,qBACL4B,KACAxB,OACAI,UACAe,mBACAV,WACAG,YACAE,uBACAd,MAAM0B,KAAK,EACX1B,MAAMiB,IAAI,EACVnB,gBAAgB6B,UAAU,EAC1BP,cACAE,gBACAX,cACA,MACA,uEAAuE;IACvE,wEAAwE;IACxE,qEAAqE;IACrE;AAEJ","ignoreList":[0]}
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
export const ACTION_REFRESH = 'refresh';
|
||||
export const ACTION_NAVIGATE = 'navigate';
|
||||
export const ACTION_RESTORE = 'restore';
|
||||
export const ACTION_SERVER_PATCH = 'server-patch';
|
||||
export const ACTION_HMR_REFRESH = 'hmr-refresh';
|
||||
export const ACTION_SERVER_ACTION = 'server-action';
|
||||
/**
|
||||
* PrefetchKind defines the type of prefetching that should be done.
|
||||
* - `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully.
|
||||
* - `full` - prefetch the page data fully.
|
||||
*/ export var PrefetchKind = /*#__PURE__*/ function(PrefetchKind) {
|
||||
PrefetchKind["AUTO"] = "auto";
|
||||
PrefetchKind["FULL"] = "full";
|
||||
return PrefetchKind;
|
||||
}({});
|
||||
/**
|
||||
* Controls the scroll behavior for a navigation.
|
||||
*/ export var ScrollBehavior = /*#__PURE__*/ function(ScrollBehavior) {
|
||||
/** Use per-node ScrollRef to decide whether to scroll. */ ScrollBehavior[ScrollBehavior["Default"] = 0] = "Default";
|
||||
/** Suppress scroll entirely (e.g. scroll={false} on Link or router.push). */ ScrollBehavior[ScrollBehavior["NoScroll"] = 1] = "NoScroll";
|
||||
return ScrollBehavior;
|
||||
}({});
|
||||
|
||||
//# sourceMappingURL=router-reducer-types.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+51
@@ -0,0 +1,51 @@
|
||||
import { ACTION_NAVIGATE, ACTION_SERVER_PATCH, ACTION_RESTORE, ACTION_REFRESH, ACTION_HMR_REFRESH, ACTION_SERVER_ACTION } from './router-reducer-types';
|
||||
import { navigateReducer } from './reducers/navigate-reducer';
|
||||
import { serverPatchReducer } from './reducers/server-patch-reducer';
|
||||
import { restoreReducer } from './reducers/restore-reducer';
|
||||
import { refreshReducer } from './reducers/refresh-reducer';
|
||||
import { hmrRefreshReducer } from './reducers/hmr-refresh-reducer';
|
||||
import { serverActionReducer } from './reducers/server-action-reducer';
|
||||
/**
|
||||
* Reducer that handles the app-router state updates.
|
||||
*/ function clientReducer(state, action) {
|
||||
switch(action.type){
|
||||
case ACTION_NAVIGATE:
|
||||
{
|
||||
return navigateReducer(state, action);
|
||||
}
|
||||
case ACTION_SERVER_PATCH:
|
||||
{
|
||||
return serverPatchReducer(state, action);
|
||||
}
|
||||
case ACTION_RESTORE:
|
||||
{
|
||||
return restoreReducer(state, action);
|
||||
}
|
||||
case ACTION_REFRESH:
|
||||
{
|
||||
return refreshReducer(state, action);
|
||||
}
|
||||
case ACTION_HMR_REFRESH:
|
||||
{
|
||||
return hmrRefreshReducer(state);
|
||||
}
|
||||
case ACTION_SERVER_ACTION:
|
||||
{
|
||||
return serverActionReducer(state, action);
|
||||
}
|
||||
// This case should never be hit as dispatch is strongly typed.
|
||||
default:
|
||||
throw Object.defineProperty(new Error('Unknown action'), "__NEXT_ERROR_CODE", {
|
||||
value: "E295",
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
function serverReducer(state, _action) {
|
||||
return state;
|
||||
}
|
||||
// we don't run the client reducer on the server, so we use a noop function for better tree shaking
|
||||
export const reducer = typeof window === 'undefined' ? serverReducer : clientReducer;
|
||||
|
||||
//# sourceMappingURL=router-reducer.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/router-reducer.ts"],"sourcesContent":["import {\n ACTION_NAVIGATE,\n ACTION_SERVER_PATCH,\n ACTION_RESTORE,\n ACTION_REFRESH,\n ACTION_HMR_REFRESH,\n ACTION_SERVER_ACTION,\n} from './router-reducer-types'\nimport type {\n ReducerActions,\n ReducerState,\n ReadonlyReducerState,\n} from './router-reducer-types'\nimport { navigateReducer } from './reducers/navigate-reducer'\nimport { serverPatchReducer } from './reducers/server-patch-reducer'\nimport { restoreReducer } from './reducers/restore-reducer'\nimport { refreshReducer } from './reducers/refresh-reducer'\nimport { hmrRefreshReducer } from './reducers/hmr-refresh-reducer'\nimport { serverActionReducer } from './reducers/server-action-reducer'\n\n/**\n * Reducer that handles the app-router state updates.\n */\nfunction clientReducer(\n state: ReadonlyReducerState,\n action: ReducerActions\n): ReducerState {\n switch (action.type) {\n case ACTION_NAVIGATE: {\n return navigateReducer(state, action)\n }\n case ACTION_SERVER_PATCH: {\n return serverPatchReducer(state, action)\n }\n case ACTION_RESTORE: {\n return restoreReducer(state, action)\n }\n case ACTION_REFRESH: {\n return refreshReducer(state, action)\n }\n case ACTION_HMR_REFRESH: {\n return hmrRefreshReducer(state)\n }\n case ACTION_SERVER_ACTION: {\n return serverActionReducer(state, action)\n }\n // This case should never be hit as dispatch is strongly typed.\n default:\n throw new Error('Unknown action')\n }\n}\n\nfunction serverReducer(\n state: ReadonlyReducerState,\n _action: ReducerActions\n): ReducerState {\n return state\n}\n\n// we don't run the client reducer on the server, so we use a noop function for better tree shaking\nexport const reducer =\n typeof window === 'undefined' ? serverReducer : clientReducer\n"],"names":["ACTION_NAVIGATE","ACTION_SERVER_PATCH","ACTION_RESTORE","ACTION_REFRESH","ACTION_HMR_REFRESH","ACTION_SERVER_ACTION","navigateReducer","serverPatchReducer","restoreReducer","refreshReducer","hmrRefreshReducer","serverActionReducer","clientReducer","state","action","type","Error","serverReducer","_action","reducer","window"],"mappings":"AAAA,SACEA,eAAe,EACfC,mBAAmB,EACnBC,cAAc,EACdC,cAAc,EACdC,kBAAkB,EAClBC,oBAAoB,QACf,yBAAwB;AAM/B,SAASC,eAAe,QAAQ,8BAA6B;AAC7D,SAASC,kBAAkB,QAAQ,kCAAiC;AACpE,SAASC,cAAc,QAAQ,6BAA4B;AAC3D,SAASC,cAAc,QAAQ,6BAA4B;AAC3D,SAASC,iBAAiB,QAAQ,iCAAgC;AAClE,SAASC,mBAAmB,QAAQ,mCAAkC;AAEtE;;CAEC,GACD,SAASC,cACPC,KAA2B,EAC3BC,MAAsB;IAEtB,OAAQA,OAAOC,IAAI;QACjB,KAAKf;YAAiB;gBACpB,OAAOM,gBAAgBO,OAAOC;YAChC;QACA,KAAKb;YAAqB;gBACxB,OAAOM,mBAAmBM,OAAOC;YACnC;QACA,KAAKZ;YAAgB;gBACnB,OAAOM,eAAeK,OAAOC;YAC/B;QACA,KAAKX;YAAgB;gBACnB,OAAOM,eAAeI,OAAOC;YAC/B;QACA,KAAKV;YAAoB;gBACvB,OAAOM,kBAAkBG;YAC3B;QACA,KAAKR;YAAsB;gBACzB,OAAOM,oBAAoBE,OAAOC;YACpC;QACA,+DAA+D;QAC/D;YACE,MAAM,qBAA2B,CAA3B,IAAIE,MAAM,mBAAV,qBAAA;uBAAA;4BAAA;8BAAA;YAA0B;IACpC;AACF;AAEA,SAASC,cACPJ,KAA2B,EAC3BK,OAAuB;IAEvB,OAAOL;AACT;AAEA,mGAAmG;AACnG,OAAO,MAAMM,UACX,OAAOC,WAAW,cAAcH,gBAAgBL,cAAa","ignoreList":[0]}
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
import { computeCacheBustingSearchParam } from '../../../shared/lib/router/utils/cache-busting-search-param';
|
||||
import { NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL, NEXT_RSC_UNION_QUERY } from '../app-router-headers';
|
||||
/**
|
||||
* Mutates the provided URL by adding a cache-busting search parameter for CDNs that don't
|
||||
* support custom headers. This helps avoid caching conflicts by making each request unique.
|
||||
*
|
||||
* Rather than relying on the Vary header which some CDNs ignore, we append a search param
|
||||
* to create a unique URL that forces a fresh request.
|
||||
*
|
||||
* Example:
|
||||
* URL before: https://example.com/path?query=1
|
||||
* URL after: https://example.com/path?query=1&_rsc=abc123
|
||||
*
|
||||
* Note: This function mutates the input URL directly and does not return anything.
|
||||
*
|
||||
* TODO: Since we need to use a search param anyway, we could simplify by removing the custom
|
||||
* headers approach entirely and just use search params.
|
||||
*/ export const setCacheBustingSearchParam = (url, headers)=>{
|
||||
const uniqueCacheKey = computeCacheBustingSearchParam(headers[NEXT_ROUTER_PREFETCH_HEADER], headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER], headers[NEXT_ROUTER_STATE_TREE_HEADER], headers[NEXT_URL]);
|
||||
setCacheBustingSearchParamWithHash(url, uniqueCacheKey);
|
||||
};
|
||||
/**
|
||||
* Sets a cache-busting search parameter on a URL using a provided hash value.
|
||||
*
|
||||
* This function performs the same logic as `setCacheBustingSearchParam` but accepts
|
||||
* a pre-computed hash instead of computing it from headers.
|
||||
*
|
||||
* Example:
|
||||
* URL before: https://example.com/path?query=1
|
||||
* hash: "abc123"
|
||||
* URL after: https://example.com/path?query=1&_rsc=abc123
|
||||
*
|
||||
* If the hash is null, we will set `_rsc` search param without a value.
|
||||
* Like this: https://example.com/path?query=1&_rsc
|
||||
*
|
||||
* Note: This function mutates the input URL directly and does not return anything.
|
||||
*/ export const setCacheBustingSearchParamWithHash = (url, hash)=>{
|
||||
/**
|
||||
* Note that we intentionally do not use `url.searchParams.set` here:
|
||||
*
|
||||
* const url = new URL('https://example.com/search?q=custom%20spacing');
|
||||
* url.searchParams.set('_rsc', 'abc123');
|
||||
* console.log(url.toString()); // Outputs: https://example.com/search?q=custom+spacing&_rsc=abc123
|
||||
* ^ <--- this is causing confusion
|
||||
* This is in fact intended based on https://url.spec.whatwg.org/#interface-urlsearchparams, but
|
||||
* we want to preserve the %20 as %20 if that's what the user passed in, hence the custom
|
||||
* logic below.
|
||||
*/ const existingSearch = url.search;
|
||||
const rawQuery = existingSearch.startsWith('?') ? existingSearch.slice(1) : existingSearch;
|
||||
// Always remove any existing cache busting param and add a fresh one to ensure
|
||||
// we have the correct value based on current request headers
|
||||
const pairs = rawQuery.split('&').filter((pair)=>pair && !pair.startsWith(`${NEXT_RSC_UNION_QUERY}=`));
|
||||
if (hash.length > 0) {
|
||||
pairs.push(`${NEXT_RSC_UNION_QUERY}=${hash}`);
|
||||
} else {
|
||||
pairs.push(`${NEXT_RSC_UNION_QUERY}`);
|
||||
}
|
||||
url.search = pairs.length ? `?${pairs.join('&')}` : '';
|
||||
};
|
||||
|
||||
//# sourceMappingURL=set-cache-busting-search-param.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/set-cache-busting-search-param.ts"],"sourcesContent":["'use client'\n\nimport { computeCacheBustingSearchParam } from '../../../shared/lib/router/utils/cache-busting-search-param'\nimport {\n NEXT_ROUTER_PREFETCH_HEADER,\n NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,\n NEXT_ROUTER_STATE_TREE_HEADER,\n NEXT_URL,\n NEXT_RSC_UNION_QUERY,\n} from '../app-router-headers'\nimport type { RequestHeaders } from './fetch-server-response'\n\n/**\n * Mutates the provided URL by adding a cache-busting search parameter for CDNs that don't\n * support custom headers. This helps avoid caching conflicts by making each request unique.\n *\n * Rather than relying on the Vary header which some CDNs ignore, we append a search param\n * to create a unique URL that forces a fresh request.\n *\n * Example:\n * URL before: https://example.com/path?query=1\n * URL after: https://example.com/path?query=1&_rsc=abc123\n *\n * Note: This function mutates the input URL directly and does not return anything.\n *\n * TODO: Since we need to use a search param anyway, we could simplify by removing the custom\n * headers approach entirely and just use search params.\n */\nexport const setCacheBustingSearchParam = (\n url: URL,\n headers: RequestHeaders\n): void => {\n const uniqueCacheKey = computeCacheBustingSearchParam(\n headers[NEXT_ROUTER_PREFETCH_HEADER],\n headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER],\n headers[NEXT_ROUTER_STATE_TREE_HEADER],\n headers[NEXT_URL]\n )\n setCacheBustingSearchParamWithHash(url, uniqueCacheKey)\n}\n\n/**\n * Sets a cache-busting search parameter on a URL using a provided hash value.\n *\n * This function performs the same logic as `setCacheBustingSearchParam` but accepts\n * a pre-computed hash instead of computing it from headers.\n *\n * Example:\n * URL before: https://example.com/path?query=1\n * hash: \"abc123\"\n * URL after: https://example.com/path?query=1&_rsc=abc123\n *\n * If the hash is null, we will set `_rsc` search param without a value.\n * Like this: https://example.com/path?query=1&_rsc\n *\n * Note: This function mutates the input URL directly and does not return anything.\n */\nexport const setCacheBustingSearchParamWithHash = (\n url: URL,\n hash: string\n): void => {\n /**\n * Note that we intentionally do not use `url.searchParams.set` here:\n *\n * const url = new URL('https://example.com/search?q=custom%20spacing');\n * url.searchParams.set('_rsc', 'abc123');\n * console.log(url.toString()); // Outputs: https://example.com/search?q=custom+spacing&_rsc=abc123\n * ^ <--- this is causing confusion\n * This is in fact intended based on https://url.spec.whatwg.org/#interface-urlsearchparams, but\n * we want to preserve the %20 as %20 if that's what the user passed in, hence the custom\n * logic below.\n */\n const existingSearch = url.search\n const rawQuery = existingSearch.startsWith('?')\n ? existingSearch.slice(1)\n : existingSearch\n\n // Always remove any existing cache busting param and add a fresh one to ensure\n // we have the correct value based on current request headers\n const pairs = rawQuery\n .split('&')\n .filter((pair) => pair && !pair.startsWith(`${NEXT_RSC_UNION_QUERY}=`))\n\n if (hash.length > 0) {\n pairs.push(`${NEXT_RSC_UNION_QUERY}=${hash}`)\n } else {\n pairs.push(`${NEXT_RSC_UNION_QUERY}`)\n }\n url.search = pairs.length ? `?${pairs.join('&')}` : ''\n}\n"],"names":["computeCacheBustingSearchParam","NEXT_ROUTER_PREFETCH_HEADER","NEXT_ROUTER_SEGMENT_PREFETCH_HEADER","NEXT_ROUTER_STATE_TREE_HEADER","NEXT_URL","NEXT_RSC_UNION_QUERY","setCacheBustingSearchParam","url","headers","uniqueCacheKey","setCacheBustingSearchParamWithHash","hash","existingSearch","search","rawQuery","startsWith","slice","pairs","split","filter","pair","length","push","join"],"mappings":"AAAA;AAEA,SAASA,8BAA8B,QAAQ,8DAA6D;AAC5G,SACEC,2BAA2B,EAC3BC,mCAAmC,EACnCC,6BAA6B,EAC7BC,QAAQ,EACRC,oBAAoB,QACf,wBAAuB;AAG9B;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,6BAA6B,CACxCC,KACAC;IAEA,MAAMC,iBAAiBT,+BACrBQ,OAAO,CAACP,4BAA4B,EACpCO,OAAO,CAACN,oCAAoC,EAC5CM,OAAO,CAACL,8BAA8B,EACtCK,OAAO,CAACJ,SAAS;IAEnBM,mCAAmCH,KAAKE;AAC1C,EAAC;AAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,qCAAqC,CAChDH,KACAI;IAEA;;;;;;;;;;GAUC,GACD,MAAMC,iBAAiBL,IAAIM,MAAM;IACjC,MAAMC,WAAWF,eAAeG,UAAU,CAAC,OACvCH,eAAeI,KAAK,CAAC,KACrBJ;IAEJ,+EAA+E;IAC/E,6DAA6D;IAC7D,MAAMK,QAAQH,SACXI,KAAK,CAAC,KACNC,MAAM,CAAC,CAACC,OAASA,QAAQ,CAACA,KAAKL,UAAU,CAAC,GAAGV,qBAAqB,CAAC,CAAC;IAEvE,IAAIM,KAAKU,MAAM,GAAG,GAAG;QACnBJ,MAAMK,IAAI,CAAC,GAAGjB,qBAAqB,CAAC,EAAEM,MAAM;IAC9C,OAAO;QACLM,MAAMK,IAAI,CAAC,GAAGjB,sBAAsB;IACtC;IACAE,IAAIM,MAAM,GAAGI,MAAMI,MAAM,GAAG,CAAC,CAAC,EAAEJ,MAAMM,IAAI,CAAC,MAAM,GAAG;AACtD,EAAC","ignoreList":[0]}
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { getNextFlightSegmentPath } from '../../flight-data-helpers';
|
||||
import { matchSegment } from '../match-segments';
|
||||
// TODO-APP: flightSegmentPath will be empty in case of static response, needs to be handled.
|
||||
export function shouldHardNavigate(flightSegmentPath, flightRouterState) {
|
||||
const [segment, parallelRoutes] = flightRouterState;
|
||||
// TODO-APP: Check if `as` can be replaced.
|
||||
const [currentSegment, parallelRouteKey] = flightSegmentPath;
|
||||
// Check if current segment matches the existing segment.
|
||||
if (!matchSegment(currentSegment, segment)) {
|
||||
// If dynamic parameter in tree doesn't match up with segment path a hard navigation is triggered.
|
||||
if (Array.isArray(currentSegment)) {
|
||||
return true;
|
||||
}
|
||||
// If the existing segment did not match soft navigation is triggered.
|
||||
return false;
|
||||
}
|
||||
const lastSegment = flightSegmentPath.length <= 2;
|
||||
if (lastSegment) {
|
||||
return false;
|
||||
}
|
||||
return shouldHardNavigate(getNextFlightSegmentPath(flightSegmentPath), parallelRoutes[parallelRouteKey]);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=should-hard-navigate.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/should-hard-navigate.ts"],"sourcesContent":["import type {\n FlightRouterState,\n FlightDataPath,\n Segment,\n} from '../../../shared/lib/app-router-types'\nimport { getNextFlightSegmentPath } from '../../flight-data-helpers'\nimport { matchSegment } from '../match-segments'\n\n// TODO-APP: flightSegmentPath will be empty in case of static response, needs to be handled.\nexport function shouldHardNavigate(\n flightSegmentPath: FlightDataPath,\n flightRouterState: FlightRouterState\n): boolean {\n const [segment, parallelRoutes] = flightRouterState\n // TODO-APP: Check if `as` can be replaced.\n const [currentSegment, parallelRouteKey] = flightSegmentPath as [\n Segment,\n string,\n ]\n\n // Check if current segment matches the existing segment.\n if (!matchSegment(currentSegment, segment)) {\n // If dynamic parameter in tree doesn't match up with segment path a hard navigation is triggered.\n if (Array.isArray(currentSegment)) {\n return true\n }\n\n // If the existing segment did not match soft navigation is triggered.\n return false\n }\n const lastSegment = flightSegmentPath.length <= 2\n\n if (lastSegment) {\n return false\n }\n\n return shouldHardNavigate(\n getNextFlightSegmentPath(flightSegmentPath),\n parallelRoutes[parallelRouteKey]\n )\n}\n"],"names":["getNextFlightSegmentPath","matchSegment","shouldHardNavigate","flightSegmentPath","flightRouterState","segment","parallelRoutes","currentSegment","parallelRouteKey","Array","isArray","lastSegment","length"],"mappings":"AAKA,SAASA,wBAAwB,QAAQ,4BAA2B;AACpE,SAASC,YAAY,QAAQ,oBAAmB;AAEhD,6FAA6F;AAC7F,OAAO,SAASC,mBACdC,iBAAiC,EACjCC,iBAAoC;IAEpC,MAAM,CAACC,SAASC,eAAe,GAAGF;IAClC,2CAA2C;IAC3C,MAAM,CAACG,gBAAgBC,iBAAiB,GAAGL;IAK3C,yDAAyD;IACzD,IAAI,CAACF,aAAaM,gBAAgBF,UAAU;QAC1C,kGAAkG;QAClG,IAAII,MAAMC,OAAO,CAACH,iBAAiB;YACjC,OAAO;QACT;QAEA,sEAAsE;QACtE,OAAO;IACT;IACA,MAAMI,cAAcR,kBAAkBS,MAAM,IAAI;IAEhD,IAAID,aAAa;QACf,OAAO;IACT;IAEA,OAAOT,mBACLF,yBAAyBG,oBACzBG,cAAc,CAACE,iBAAiB;AAEpC","ignoreList":[0]}
|
||||
Reference in New Issue
Block a user