including-modules
This commit is contained in:
Generated
Vendored
+98
@@ -0,0 +1,98 @@
|
||||
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];
|
||||
};
|
||||
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 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
+43
@@ -0,0 +1,43 @@
|
||||
import { createHrefFromUrl } from './create-href-from-url';
|
||||
import { extractPathFromFlightRouterState } from './compute-changed-path';
|
||||
import { getFlightDataPartsFromPath } from '../../flight-data-helpers';
|
||||
import { createInitialCacheNodeForHydration } from './ppr-navigations';
|
||||
export function createInitialRouterState({ navigatedAt, initialFlightData, initialCanonicalUrlParts, initialRenderedSearch, location }) {
|
||||
// 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;
|
||||
const initialState = {
|
||||
tree: initialTree,
|
||||
cache: createInitialCacheNodeForHydration(navigatedAt, initialTree, initialSeedData, initialHead),
|
||||
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: {
|
||||
apply: false,
|
||||
onlyHashChange: false,
|
||||
hashFragment: null,
|
||||
segmentPaths: []
|
||||
},
|
||||
canonicalUrl,
|
||||
renderedSearch: initialRenderedSearch,
|
||||
nextUrl: // the || operator is intentional, the pathname can be an empty string
|
||||
(extractPathFromFlightRouterState(initialTree) || location?.pathname) ?? null,
|
||||
previousNextUrl: null,
|
||||
debugInfo: null
|
||||
};
|
||||
return initialState;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=create-initial-router-state.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../src/client/components/router-reducer/create-initial-router-state.ts"],"sourcesContent":["import type { FlightDataPath } from '../../../shared/lib/app-router-types'\n\nimport { createHrefFromUrl } from './create-href-from-url'\nimport { extractPathFromFlightRouterState } from './compute-changed-path'\n\nimport type { AppRouterState } from './router-reducer-types'\nimport { getFlightDataPartsFromPath } from '../../flight-data-helpers'\nimport { createInitialCacheNodeForHydration } from './ppr-navigations'\n\nexport interface InitialRouterStateParameters {\n navigatedAt: number\n initialCanonicalUrlParts: string[]\n initialRenderedSearch: string\n initialFlightData: FlightDataPath[]\n location: Location | null\n}\n\nexport function createInitialRouterState({\n navigatedAt,\n initialFlightData,\n initialCanonicalUrlParts,\n initialRenderedSearch,\n location,\n}: InitialRouterStateParameters): AppRouterState {\n // When initialized on the server, the canonical URL is provided as an array of parts.\n // This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it\n // as a URL that should be crawled.\n const initialCanonicalUrl = initialCanonicalUrlParts.join('/')\n\n const normalizedFlightData = getFlightDataPartsFromPath(initialFlightData[0])\n const {\n tree: initialTree,\n seedData: initialSeedData,\n head: initialHead,\n } = normalizedFlightData\n // For the SSR render, seed data should always be available (we only send back a `null` response\n // in the case of a `loading` segment, pre-PPR.)\n\n const canonicalUrl =\n // location.href is read as the initial value for canonicalUrl in the browser\n // 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.\n location\n ? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs.\n createHrefFromUrl(location)\n : initialCanonicalUrl\n\n const initialState = {\n tree: initialTree,\n cache: createInitialCacheNodeForHydration(\n navigatedAt,\n initialTree,\n initialSeedData,\n initialHead\n ),\n pushRef: {\n pendingPush: false,\n mpaNavigation: false,\n // First render needs to preserve the previous window.history.state\n // to avoid it being overwritten on navigation back/forward with MPA Navigation.\n preserveCustomHistoryState: true,\n },\n focusAndScrollRef: {\n apply: false,\n onlyHashChange: false,\n hashFragment: null,\n segmentPaths: [],\n },\n canonicalUrl,\n renderedSearch: initialRenderedSearch,\n nextUrl:\n // the || operator is intentional, the pathname can be an empty string\n (extractPathFromFlightRouterState(initialTree) || location?.pathname) ??\n null,\n previousNextUrl: null,\n debugInfo: null,\n }\n\n return initialState\n}\n"],"names":["createHrefFromUrl","extractPathFromFlightRouterState","getFlightDataPartsFromPath","createInitialCacheNodeForHydration","createInitialRouterState","navigatedAt","initialFlightData","initialCanonicalUrlParts","initialRenderedSearch","location","initialCanonicalUrl","join","normalizedFlightData","tree","initialTree","seedData","initialSeedData","head","initialHead","canonicalUrl","initialState","cache","pushRef","pendingPush","mpaNavigation","preserveCustomHistoryState","focusAndScrollRef","apply","onlyHashChange","hashFragment","segmentPaths","renderedSearch","nextUrl","pathname","previousNextUrl","debugInfo"],"mappings":"AAEA,SAASA,iBAAiB,QAAQ,yBAAwB;AAC1D,SAASC,gCAAgC,QAAQ,yBAAwB;AAGzE,SAASC,0BAA0B,QAAQ,4BAA2B;AACtE,SAASC,kCAAkC,QAAQ,oBAAmB;AAUtE,OAAO,SAASC,yBAAyB,EACvCC,WAAW,EACXC,iBAAiB,EACjBC,wBAAwB,EACxBC,qBAAqB,EACrBC,QAAQ,EACqB;IAC7B,sFAAsF;IACtF,kGAAkG;IAClG,mCAAmC;IACnC,MAAMC,sBAAsBH,yBAAyBI,IAAI,CAAC;IAE1D,MAAMC,uBAAuBV,2BAA2BI,iBAAiB,CAAC,EAAE;IAC5E,MAAM,EACJO,MAAMC,WAAW,EACjBC,UAAUC,eAAe,EACzBC,MAAMC,WAAW,EAClB,GAAGN;IACJ,gGAAgG;IAChG,gDAAgD;IAEhD,MAAMO,eACJ,6EAA6E;IAC7E,kJAAkJ;IAClJV,WAEIT,kBAAkBS,YAClBC;IAEN,MAAMU,eAAe;QACnBP,MAAMC;QACNO,OAAOlB,mCACLE,aACAS,aACAE,iBACAE;QAEFI,SAAS;YACPC,aAAa;YACbC,eAAe;YACf,mEAAmE;YACnE,gFAAgF;YAChFC,4BAA4B;QAC9B;QACAC,mBAAmB;YACjBC,OAAO;YACPC,gBAAgB;YAChBC,cAAc;YACdC,cAAc,EAAE;QAClB;QACAX;QACAY,gBAAgBvB;QAChBwB,SAEE,AADA,sEAAsE;QACrE/B,CAAAA,iCAAiCa,gBAAgBL,UAAUwB,QAAO,KACnE;QACFC,iBAAiB;QACjBC,WAAW;IACb;IAEA,OAAOf;AACT","ignoreList":[0]}
|
||||
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
+313
@@ -0,0 +1,313 @@
|
||||
'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 { 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_ROUTER_STALE_TIME_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 { getAppBuildId } from '../../app-build-id';
|
||||
import { setCacheBustingSearchParam } from './set-cache-busting-search-param';
|
||||
import { getRenderedSearch, urlToUrlWithoutFlightMarker } from '../../route-params';
|
||||
import { getDeploymentId } from '../../../shared/lib/deployment-id';
|
||||
const createFromReadableStream = createFromReadableStreamBrowser;
|
||||
const createFromFetch = createFromFetchBrowser;
|
||||
let createDebugChannel;
|
||||
if (process.env.NODE_ENV !== 'production' && 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);
|
||||
const staleTimeHeaderSeconds = res.headers.get(NEXT_ROUTER_STALE_TIME_HEADER);
|
||||
const staleTime = staleTimeHeaderSeconds !== null ? parseInt(staleTimeHeaderSeconds, 10) * 1000 : -1;
|
||||
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.flightResponse;
|
||||
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.
|
||||
const flightStream = postponed ? createUnclosingPrefetchStream(res.body) : res.body;
|
||||
flightResponsePromise = createFromNextReadableStream(flightStream, headers);
|
||||
}
|
||||
const flightResponse = await flightResponsePromise;
|
||||
if (getAppBuildId() !== flightResponse.b) {
|
||||
return doMpaNavigation(res.url);
|
||||
}
|
||||
const normalizedFlightData = normalizeFlightData(flightResponse.f);
|
||||
if (typeof normalizedFlightData === 'string') {
|
||||
return doMpaNavigation(normalizedFlightData);
|
||||
}
|
||||
return {
|
||||
flightData: normalizedFlightData,
|
||||
canonicalUrl: canonicalUrl,
|
||||
renderedSearch: getRenderedSearch(res),
|
||||
couldBeIntercepted: interception,
|
||||
prerendered: flightResponse.S,
|
||||
postponed,
|
||||
staleTime,
|
||||
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();
|
||||
}
|
||||
}
|
||||
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.NODE_ENV !== 'production') {
|
||||
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 fetchPromise = fetch(fetchUrl, fetchOptions);
|
||||
// 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 we have
|
||||
// to do some extra processing of the response stream (see
|
||||
// `createUnclosingPrefetchStream`). But this is fine, 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);
|
||||
fetchPromise = fetch(fetchUrl, fetchOptions);
|
||||
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.
|
||||
flightResponse: flightResponsePromise
|
||||
};
|
||||
return rscResponse;
|
||||
}
|
||||
export function createFromNextReadableStream(flightStream, requestHeaders) {
|
||||
return createFromReadableStream(flightStream, {
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
debugChannel: createDebugChannel && createDebugChannel(requestHeaders)
|
||||
});
|
||||
}
|
||||
function createFromNextFetch(promiseForResponse, requestHeaders) {
|
||||
return createFromFetch(promiseForResponse, {
|
||||
callServer,
|
||||
findSourceMapURL,
|
||||
debugChannel: createDebugChannel && createDebugChannel(requestHeaders)
|
||||
});
|
||||
}
|
||||
function createUnclosingPrefetchStream(originalFlightStream) {
|
||||
// When PPR is enabled, prefetch streams may contain references that never
|
||||
// resolve, because that's how we encode dynamic data access. In the decoded
|
||||
// object returned by the Flight client, these are reified into hanging
|
||||
// promises that suspend during render, which is effectively what we want.
|
||||
// The UI resolves when it switches to the dynamic data stream
|
||||
// (via useDeferredValue(dynamic, static)).
|
||||
//
|
||||
// However, the Flight implementation currently errors if the server closes
|
||||
// the response before all the references are resolved. As a cheat to work
|
||||
// around this, we wrap the original stream in a new stream that never closes,
|
||||
// and therefore doesn't error.
|
||||
const reader = originalFlightStream.getReader();
|
||||
return new ReadableStream({
|
||||
async pull (controller) {
|
||||
while(true){
|
||||
const { done, value } = await reader.read();
|
||||
if (!done) {
|
||||
// Pass to the target stream and keep consuming the Flight response
|
||||
// from the server.
|
||||
controller.enqueue(value);
|
||||
continue;
|
||||
}
|
||||
// The server stream has closed. Exit, but intentionally do not close
|
||||
// the target stream.
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=fetch-server-response.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+50
@@ -0,0 +1,50 @@
|
||||
import { computeChangedPath } from './compute-changed-path';
|
||||
function isNotUndefined(value) {
|
||||
return typeof value !== 'undefined';
|
||||
}
|
||||
export function handleMutable(state, mutable) {
|
||||
// shouldScroll is true by default, can override to false.
|
||||
const shouldScroll = mutable.shouldScroll ?? true;
|
||||
let previousNextUrl = state.previousNextUrl;
|
||||
let nextUrl = state.nextUrl;
|
||||
if (isNotUndefined(mutable.patchedTree)) {
|
||||
// If we received a patched tree, we need to compute the changed path.
|
||||
const changedPath = computeChangedPath(state.tree, mutable.patchedTree);
|
||||
if (changedPath) {
|
||||
// If the tree changed, we need to update the nextUrl
|
||||
previousNextUrl = nextUrl;
|
||||
nextUrl = changedPath;
|
||||
} else if (!nextUrl) {
|
||||
// if the tree ends up being the same (ie, no changed path), and we don't have a nextUrl, then we should use the canonicalUrl
|
||||
nextUrl = state.canonicalUrl;
|
||||
}
|
||||
// otherwise this will be a no-op and continue to use the existing nextUrl
|
||||
}
|
||||
return {
|
||||
// Set href.
|
||||
canonicalUrl: mutable.canonicalUrl ?? state.canonicalUrl,
|
||||
renderedSearch: mutable.renderedSearch ?? state.renderedSearch,
|
||||
pushRef: {
|
||||
pendingPush: isNotUndefined(mutable.pendingPush) ? mutable.pendingPush : state.pushRef.pendingPush,
|
||||
mpaNavigation: isNotUndefined(mutable.mpaNavigation) ? mutable.mpaNavigation : state.pushRef.mpaNavigation,
|
||||
preserveCustomHistoryState: isNotUndefined(mutable.preserveCustomHistoryState) ? mutable.preserveCustomHistoryState : state.pushRef.preserveCustomHistoryState
|
||||
},
|
||||
// All navigation requires scroll and focus management to trigger.
|
||||
focusAndScrollRef: {
|
||||
apply: shouldScroll ? isNotUndefined(mutable?.scrollableSegments) ? true : state.focusAndScrollRef.apply : false,
|
||||
onlyHashChange: mutable.onlyHashChange || false,
|
||||
hashFragment: shouldScroll ? // #top is handled in layout-router.
|
||||
mutable.hashFragment && mutable.hashFragment !== '' ? decodeURIComponent(mutable.hashFragment.slice(1)) : state.focusAndScrollRef.hashFragment : null,
|
||||
segmentPaths: shouldScroll ? mutable?.scrollableSegments ?? state.focusAndScrollRef.segmentPaths : []
|
||||
},
|
||||
// Apply cache.
|
||||
cache: mutable.cache ? mutable.cache : state.cache,
|
||||
// Apply patched router state.
|
||||
tree: isNotUndefined(mutable.patchedTree) ? mutable.patchedTree : state.tree,
|
||||
nextUrl,
|
||||
previousNextUrl: previousNextUrl,
|
||||
debugInfo: mutable.collectedDebugInfo ?? null
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=handle-mutable.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
export function isNavigatingToNewRootLayout(currentTree, nextTree) {
|
||||
// Compare segments
|
||||
const currentTreeSegment = currentTree[0];
|
||||
const nextTreeSegment = nextTree[0];
|
||||
// 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
|
||||
if (currentTree[4]) {
|
||||
// If the next tree doesn't have the root layout flag, it must have changed.
|
||||
return !nextTree[4];
|
||||
}
|
||||
// Current tree didn't have its root layout here, must have changed.
|
||||
if (nextTree[4]) {
|
||||
return true;
|
||||
}
|
||||
// We can't assume it's `parallelRoutes.children` here in case the root layout is `app/@something/layout.js`
|
||||
// But it's not possible to be more than one parallelRoutes before the root layout is found
|
||||
// TODO-APP: change to traverse all parallel routes
|
||||
const currentTreeChild = Object.values(currentTree[1])[0];
|
||||
const nextTreeChild = Object.values(nextTree[1])[0];
|
||||
if (!currentTreeChild || !nextTreeChild) return true;
|
||||
return isNavigatingToNewRootLayout(currentTreeChild, nextTreeChild);
|
||||
}
|
||||
|
||||
//# 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'\n\nexport function isNavigatingToNewRootLayout(\n currentTree: FlightRouterState,\n nextTree: FlightRouterState\n): boolean {\n // Compare segments\n const currentTreeSegment = currentTree[0]\n const nextTreeSegment = nextTree[0]\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 if (currentTree[4]) {\n // If the next tree doesn't have the root layout flag, it must have changed.\n return !nextTree[4]\n }\n // Current tree didn't have its root layout here, must have changed.\n if (nextTree[4]) {\n return true\n }\n // We can't assume it's `parallelRoutes.children` here in case the root layout is `app/@something/layout.js`\n // But it's not possible to be more than one parallelRoutes before the root layout is found\n // TODO-APP: change to traverse all parallel routes\n const currentTreeChild = Object.values(currentTree[1])[0]\n const nextTreeChild = Object.values(nextTree[1])[0]\n if (!currentTreeChild || !nextTreeChild) return true\n return isNavigatingToNewRootLayout(currentTreeChild, nextTreeChild)\n}\n"],"names":["isNavigatingToNewRootLayout","currentTree","nextTree","currentTreeSegment","nextTreeSegment","Array","isArray","currentTreeChild","Object","values","nextTreeChild"],"mappings":"AAEA,OAAO,SAASA,4BACdC,WAA8B,EAC9BC,QAA2B;IAE3B,mBAAmB;IACnB,MAAMC,qBAAqBF,WAAW,CAAC,EAAE;IACzC,MAAMG,kBAAkBF,QAAQ,CAAC,EAAE;IAEnC,2FAA2F;IAC3F,4DAA4D;IAC5D,uIAAuI;IACvI,IAAIG,MAAMC,OAAO,CAACH,uBAAuBE,MAAMC,OAAO,CAACF,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,IAAIH,WAAW,CAAC,EAAE,EAAE;QAClB,4EAA4E;QAC5E,OAAO,CAACC,QAAQ,CAAC,EAAE;IACrB;IACA,oEAAoE;IACpE,IAAIA,QAAQ,CAAC,EAAE,EAAE;QACf,OAAO;IACT;IACA,4GAA4G;IAC5G,2FAA2F;IAC3F,mDAAmD;IACnD,MAAMK,mBAAmBC,OAAOC,MAAM,CAACR,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE;IACzD,MAAMS,gBAAgBF,OAAOC,MAAM,CAACP,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE;IACnD,IAAI,CAACK,oBAAoB,CAACG,eAAe,OAAO;IAChD,OAAOV,4BAA4BO,kBAAkBG;AACvD","ignoreList":[0]}
|
||||
+1137
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
+48
@@ -0,0 +1,48 @@
|
||||
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');
|
||||
}
|
||||
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 childSegmentMap = cache.parallelRoutes.get(key);
|
||||
if (!childSegmentMap) {
|
||||
continue;
|
||||
}
|
||||
const cacheKey = createRouterCacheKey(segment);
|
||||
const cacheKeyWithoutSearchParams = createRouterCacheKey(segment, true);
|
||||
const cacheNode = childSegmentMap.get(cacheKey);
|
||||
if (!cacheNode) {
|
||||
continue;
|
||||
}
|
||||
const item = findHeadInCacheImpl(cacheNode, 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 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 const childSegmentMap = cache.parallelRoutes.get(key)\n if (!childSegmentMap) {\n continue\n }\n\n const cacheKey = createRouterCacheKey(segment)\n const cacheKeyWithoutSearchParams = createRouterCacheKey(segment, true)\n\n const cacheNode = childSegmentMap.get(cacheKey)\n if (!cacheNode) {\n continue\n }\n\n const item = findHeadInCacheImpl(\n cacheNode,\n childParallelRoutes,\n keyPrefix + '/' + cacheKey,\n keyPrefix + '/' + cacheKeyWithoutSearchParams\n )\n\n if (item) {\n return item\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","segment","childParallelRoutes","childSegmentMap","get","cacheKey","cacheKeyWithoutSearchParams","cacheNode","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,KAAK,MAAMD,OAAOF,mBAAoB;QACpC,MAAM,CAACI,SAASC,oBAAoB,GAAGb,cAAc,CAACU,IAAI;QAC1D,gEAAgE;QAChE,mCAAmC;QACnC,IAAIE,YAAYhB,qBAAqB;YACnC;QACF;QACA,MAAMkB,kBAAkBf,MAAMC,cAAc,CAACe,GAAG,CAACL;QACjD,IAAI,CAACI,iBAAiB;YACpB;QACF;QAEA,MAAME,WAAWnB,qBAAqBe;QACtC,MAAMK,8BAA8BpB,qBAAqBe,SAAS;QAElE,MAAMM,YAAYJ,gBAAgBC,GAAG,CAACC;QACtC,IAAI,CAACE,WAAW;YACd;QACF;QAEA,MAAMC,OAAOlB,oBACXiB,WACAL,qBACAX,YAAY,MAAMc,UAClBd,YAAY,MAAMe;QAGpB,IAAIE,MAAM;YACR,OAAOA;QACT;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
+126
@@ -0,0 +1,126 @@
|
||||
import { createHrefFromUrl } from '../create-href-from-url';
|
||||
import { handleMutable } from '../handle-mutable';
|
||||
import { navigate as navigateUsingSegmentCache } from '../../segment-cache/navigation';
|
||||
import { NavigationResultTag } from '../../segment-cache/types';
|
||||
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 handleExternalUrl(state, mutable, url, pendingPush) {
|
||||
mutable.mpaNavigation = true;
|
||||
mutable.canonicalUrl = url;
|
||||
mutable.pendingPush = pendingPush;
|
||||
mutable.scrollableSegments = undefined;
|
||||
return handleMutable(state, mutable);
|
||||
}
|
||||
export function generateSegmentsFromPatch(flightRouterPatch) {
|
||||
const segments = [];
|
||||
const [segment, parallelRoutes] = flightRouterPatch;
|
||||
if (Object.keys(parallelRoutes).length === 0) {
|
||||
return [
|
||||
[
|
||||
segment
|
||||
]
|
||||
];
|
||||
}
|
||||
for (const [parallelRouteKey, parallelRoute] of Object.entries(parallelRoutes)){
|
||||
for (const childSegment of generateSegmentsFromPatch(parallelRoute)){
|
||||
// If the segment is empty, it means we are at the root of the tree
|
||||
if (segment === '') {
|
||||
segments.push([
|
||||
parallelRouteKey,
|
||||
...childSegment
|
||||
]);
|
||||
} else {
|
||||
segments.push([
|
||||
segment,
|
||||
parallelRouteKey,
|
||||
...childSegment
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
export function handleNavigationResult(url, state, mutable, pendingPush, result) {
|
||||
switch(result.tag){
|
||||
case NavigationResultTag.MPA:
|
||||
{
|
||||
// Perform an MPA navigation.
|
||||
const newUrl = result.data;
|
||||
return handleExternalUrl(state, mutable, newUrl, pendingPush);
|
||||
}
|
||||
case NavigationResultTag.Success:
|
||||
{
|
||||
// Received a new result.
|
||||
mutable.cache = result.data.cacheNode;
|
||||
mutable.patchedTree = result.data.flightRouterState;
|
||||
mutable.renderedSearch = result.data.renderedSearch;
|
||||
mutable.canonicalUrl = result.data.canonicalUrl;
|
||||
// TODO: During a refresh, we don't set the `scrollableSegments`. There's
|
||||
// some confusing and subtle logic in `handleMutable` that decides what
|
||||
// to do when `shouldScroll` is set but `scrollableSegments` is not. I'm
|
||||
// not convinced it's totally coherent but the tests assert on this
|
||||
// particular behavior so I've ported the logic as-is from the previous
|
||||
// router implementation, for now.
|
||||
mutable.scrollableSegments = result.data.scrollableSegments ?? undefined;
|
||||
mutable.shouldScroll = result.data.shouldScroll;
|
||||
mutable.hashFragment = result.data.hash;
|
||||
// Check if the only thing that changed was the hash fragment.
|
||||
const oldUrl = new URL(state.canonicalUrl, url);
|
||||
const onlyHashChange = // We don't need to compare the origins, because client-driven
|
||||
// navigations are always same-origin.
|
||||
url.pathname === oldUrl.pathname && url.search === oldUrl.search && url.hash !== oldUrl.hash;
|
||||
if (onlyHashChange) {
|
||||
// The only updated part of the URL is the hash.
|
||||
mutable.onlyHashChange = true;
|
||||
mutable.shouldScroll = result.data.shouldScroll;
|
||||
mutable.hashFragment = url.hash;
|
||||
// Setting this to an empty array triggers a scroll for all new and
|
||||
// updated segments. See `ScrollAndFocusHandler` for more details.
|
||||
mutable.scrollableSegments = [];
|
||||
}
|
||||
return handleMutable(state, mutable);
|
||||
}
|
||||
case NavigationResultTag.Async:
|
||||
{
|
||||
return result.data.then((asyncResult)=>handleNavigationResult(url, state, mutable, pendingPush, asyncResult), // If the navigation failed, return the current state.
|
||||
// TODO: This matches the current behavior but we need to do something
|
||||
// better here if the network fails.
|
||||
()=>{
|
||||
return state;
|
||||
});
|
||||
}
|
||||
default:
|
||||
{
|
||||
result;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function navigateReducer(state, action) {
|
||||
const { url, isExternalUrl, navigateType, shouldScroll } = action;
|
||||
const mutable = {};
|
||||
const href = createHrefFromUrl(url);
|
||||
const pendingPush = navigateType === 'push';
|
||||
mutable.preserveCustomHistoryState = false;
|
||||
mutable.pendingPush = pendingPush;
|
||||
if (isExternalUrl) {
|
||||
return handleExternalUrl(state, mutable, url.toString(), pendingPush);
|
||||
}
|
||||
// Handles case where `<meta http-equiv="refresh">` tag is present,
|
||||
// which will trigger an MPA navigation.
|
||||
if (document.getElementById('__next-page-redirect')) {
|
||||
return handleExternalUrl(state, mutable, href, pendingPush);
|
||||
}
|
||||
// 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 result = navigateUsingSegmentCache(url, currentUrl, state.cache, state.tree, state.nextUrl, FreshnessPolicy.Default, shouldScroll, mutable);
|
||||
return handleNavigationResult(url, state, mutable, pendingPush, result);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=navigate-reducer.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import { handleNavigationResult } from './navigate-reducer';
|
||||
import { navigateToSeededRoute } from '../../segment-cache/navigation';
|
||||
import { revalidateEntireCache } from '../../segment-cache/cache';
|
||||
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
export function refreshReducer(state) {
|
||||
// TODO: Currently, all refreshes purge the prefetch cache. In the future,
|
||||
// only client-side refreshes will have this behavior; the server-side
|
||||
// `refresh` should send new data without purging the prefetch cache.
|
||||
const currentNextUrl = state.nextUrl;
|
||||
const currentRouterState = state.tree;
|
||||
revalidateEntireCache(currentNextUrl, currentRouterState);
|
||||
return refreshDynamicData(state, FreshnessPolicy.RefreshAll);
|
||||
}
|
||||
export function refreshDynamicData(state, freshnessPolicy) {
|
||||
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 currentFlightRouterState = state.tree;
|
||||
const shouldScroll = true;
|
||||
const navigationSeed = {
|
||||
tree: state.tree,
|
||||
renderedSearch: state.renderedSearch,
|
||||
data: null,
|
||||
head: null
|
||||
};
|
||||
const now = Date.now();
|
||||
const result = navigateToSeededRoute(now, currentUrl, currentCanonicalUrl, navigationSeed, currentUrl, state.cache, currentFlightRouterState, freshnessPolicy, nextUrlForRefresh, shouldScroll);
|
||||
const mutable = {};
|
||||
mutable.preserveCustomHistoryState = false;
|
||||
return handleNavigationResult(currentUrl, state, mutable, false, result);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=refresh-reducer.js.map
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../../../../src/client/components/router-reducer/reducers/refresh-reducer.ts"],"sourcesContent":["import type {\n Mutable,\n ReadonlyReducerState,\n ReducerState,\n} from '../router-reducer-types'\nimport { handleNavigationResult } from './navigate-reducer'\nimport { navigateToSeededRoute } from '../../segment-cache/navigation'\nimport { revalidateEntireCache } from '../../segment-cache/cache'\nimport { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree'\nimport { FreshnessPolicy } from '../ppr-navigations'\n\nexport function refreshReducer(state: ReadonlyReducerState): ReducerState {\n // TODO: Currently, all refreshes purge the prefetch cache. In the future,\n // only client-side refreshes will have this behavior; the server-side\n // `refresh` should send new data without purging the prefetch cache.\n const currentNextUrl = state.nextUrl\n const currentRouterState = state.tree\n revalidateEntireCache(currentNextUrl, currentRouterState)\n return refreshDynamicData(state, FreshnessPolicy.RefreshAll)\n}\n\nexport function refreshDynamicData(\n state: ReadonlyReducerState,\n freshnessPolicy: FreshnessPolicy.RefreshAll | FreshnessPolicy.HMRRefresh\n): ReducerState {\n const currentNextUrl = state.nextUrl\n\n // We always send the last next-url, not the current when performing a dynamic\n // request. This is because we update the next-url after a navigation, but we\n // want the same interception route to be matched that used the last next-url.\n const nextUrlForRefresh = hasInterceptionRouteInCurrentTree(state.tree)\n ? state.previousNextUrl || currentNextUrl\n : null\n\n // A refresh is modeled as a navigation to the current URL, but where any\n // existing dynamic data (including in shared layouts) is re-fetched.\n const currentCanonicalUrl = state.canonicalUrl\n const currentUrl = new URL(currentCanonicalUrl, location.origin)\n const currentFlightRouterState = state.tree\n const shouldScroll = true\n\n const navigationSeed = {\n tree: state.tree,\n renderedSearch: state.renderedSearch,\n data: null,\n head: null,\n }\n\n const now = Date.now()\n const result = navigateToSeededRoute(\n now,\n currentUrl,\n currentCanonicalUrl,\n navigationSeed,\n currentUrl,\n state.cache,\n currentFlightRouterState,\n freshnessPolicy,\n nextUrlForRefresh,\n shouldScroll\n )\n\n const mutable: Mutable = {}\n mutable.preserveCustomHistoryState = false\n\n return handleNavigationResult(currentUrl, state, mutable, false, result)\n}\n"],"names":["handleNavigationResult","navigateToSeededRoute","revalidateEntireCache","hasInterceptionRouteInCurrentTree","FreshnessPolicy","refreshReducer","state","currentNextUrl","nextUrl","currentRouterState","tree","refreshDynamicData","RefreshAll","freshnessPolicy","nextUrlForRefresh","previousNextUrl","currentCanonicalUrl","canonicalUrl","currentUrl","URL","location","origin","currentFlightRouterState","shouldScroll","navigationSeed","renderedSearch","data","head","now","Date","result","cache","mutable","preserveCustomHistoryState"],"mappings":"AAKA,SAASA,sBAAsB,QAAQ,qBAAoB;AAC3D,SAASC,qBAAqB,QAAQ,iCAAgC;AACtE,SAASC,qBAAqB,QAAQ,4BAA2B;AACjE,SAASC,iCAAiC,QAAQ,2CAA0C;AAC5F,SAASC,eAAe,QAAQ,qBAAoB;AAEpD,OAAO,SAASC,eAAeC,KAA2B;IACxD,0EAA0E;IAC1E,sEAAsE;IACtE,qEAAqE;IACrE,MAAMC,iBAAiBD,MAAME,OAAO;IACpC,MAAMC,qBAAqBH,MAAMI,IAAI;IACrCR,sBAAsBK,gBAAgBE;IACtC,OAAOE,mBAAmBL,OAAOF,gBAAgBQ,UAAU;AAC7D;AAEA,OAAO,SAASD,mBACdL,KAA2B,EAC3BO,eAAwE;IAExE,MAAMN,iBAAiBD,MAAME,OAAO;IAEpC,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,MAAMM,oBAAoBX,kCAAkCG,MAAMI,IAAI,IAClEJ,MAAMS,eAAe,IAAIR,iBACzB;IAEJ,yEAAyE;IACzE,qEAAqE;IACrE,MAAMS,sBAAsBV,MAAMW,YAAY;IAC9C,MAAMC,aAAa,IAAIC,IAAIH,qBAAqBI,SAASC,MAAM;IAC/D,MAAMC,2BAA2BhB,MAAMI,IAAI;IAC3C,MAAMa,eAAe;IAErB,MAAMC,iBAAiB;QACrBd,MAAMJ,MAAMI,IAAI;QAChBe,gBAAgBnB,MAAMmB,cAAc;QACpCC,MAAM;QACNC,MAAM;IACR;IAEA,MAAMC,MAAMC,KAAKD,GAAG;IACpB,MAAME,SAAS7B,sBACb2B,KACAV,YACAF,qBACAQ,gBACAN,YACAZ,MAAMyB,KAAK,EACXT,0BACAT,iBACAC,mBACAS;IAGF,MAAMS,UAAmB,CAAC;IAC1BA,QAAQC,0BAA0B,GAAG;IAErC,OAAOjC,uBAAuBkB,YAAYZ,OAAO0B,SAAS,OAAOF;AACnE","ignoreList":[0]}
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
import { createHrefFromUrl } from '../create-href-from-url';
|
||||
import { extractPathFromFlightRouterState } from '../compute-changed-path';
|
||||
import { FreshnessPolicy, spawnDynamicRequests, startPPRNavigation } from '../ppr-navigations';
|
||||
import { handleExternalUrl } from './navigate-reducer';
|
||||
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 restoredCanonicalUrl = createHrefFromUrl(restoredUrl);
|
||||
const restoredNextUrl = extractPathFromFlightRouterState(treeToRestore) ?? restoredUrl.pathname;
|
||||
const now = Date.now();
|
||||
const accumulation = {
|
||||
scrollableSegments: null,
|
||||
separateRefreshUrls: null
|
||||
};
|
||||
const task = startPPRNavigation(now, currentUrl, state.cache, state.tree, treeToRestore, FreshnessPolicy.HistoryTraversal, null, null, null, null, false, false, accumulation);
|
||||
if (task === null) {
|
||||
const mutable = {
|
||||
preserveCustomHistoryState: true
|
||||
};
|
||||
return handleExternalUrl(state, mutable, restoredCanonicalUrl, false);
|
||||
}
|
||||
spawnDynamicRequests(task, restoredUrl, restoredNextUrl, FreshnessPolicy.HistoryTraversal, accumulation);
|
||||
return {
|
||||
// Set canonical url
|
||||
canonicalUrl: restoredCanonicalUrl,
|
||||
renderedSearch,
|
||||
pushRef: {
|
||||
pendingPush: false,
|
||||
mpaNavigation: false,
|
||||
// Ensures that the custom history state that was set is preserved when applying this update.
|
||||
preserveCustomHistoryState: true
|
||||
},
|
||||
focusAndScrollRef: state.focusAndScrollRef,
|
||||
cache: task.node,
|
||||
// Restore provided tree
|
||||
tree: treeToRestore,
|
||||
nextUrl: restoredNextUrl,
|
||||
// TODO: We need to restore previousNextUrl, too, which represents the
|
||||
// Next-Url that was used to fetch the data. Anywhere we fetch using the
|
||||
// canonical URL, there should be a corresponding Next-Url.
|
||||
previousNextUrl: null,
|
||||
debugInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=restore-reducer.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+281
@@ -0,0 +1,281 @@
|
||||
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 { assignLocation } from '../../../assign-location';
|
||||
import { createHrefFromUrl } from '../create-href-from-url';
|
||||
import { handleExternalUrl, handleNavigationResult } from './navigate-reducer';
|
||||
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree';
|
||||
import { normalizeFlightData, prepareFlightRouterStateForRequest } from '../../../flight-data-helpers';
|
||||
import { getRedirectError } from '../../redirect';
|
||||
import { RedirectType } from '../../redirect-error';
|
||||
import { removeBasePath } from '../../../remove-base-path';
|
||||
import { hasBasePath } from '../../../has-base-path';
|
||||
import { extractInfoFromServerReferenceId, omitUnusedArgs } from '../../../../shared/lib/server-reference-info';
|
||||
import { revalidateEntireCache } from '../../segment-cache/cache';
|
||||
import { getDeploymentId } from '../../../../shared/lib/deployment-id';
|
||||
import { navigateToSeededRoute, navigate as navigateUsingSegmentCache } from '../../segment-cache/navigation';
|
||||
import { ActionDidNotRevalidate, ActionDidRevalidateDynamicOnly, ActionDidRevalidateStaticAndDynamic } from '../../../../shared/lib/action-revalidation-kind';
|
||||
import { isExternalURL } from '../../app-router-utils';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
const createFromFetch = createFromFetchBrowser;
|
||||
let createDebugChannel;
|
||||
if (process.env.NODE_ENV !== 'production' && 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);
|
||||
// TODO: Currently, we're only omitting unused args for the experimental "use
|
||||
// cache" functions. Once the server reference info byte feature is stable, we
|
||||
// should apply this to server actions as well.
|
||||
const usedArgs = info.type === 'use-cache' ? omitUnusedArgs(actionArgs, info) : actionArgs;
|
||||
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.NODE_ENV !== 'production') {
|
||||
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 = RedirectType.push;
|
||||
break;
|
||||
case 'replace':
|
||||
redirectType = 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 actionFlightDataCouldBeIntercepted;
|
||||
if (isRscResponse) {
|
||||
const response = await createFromFetch(Promise.resolve(res), {
|
||||
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;
|
||||
const maybeFlightData = normalizeFlightData(response.f);
|
||||
if (maybeFlightData !== '') {
|
||||
actionFlightData = maybeFlightData;
|
||||
actionFlightDataRenderedSearch = response.q;
|
||||
actionFlightDataCouldBeIntercepted = response.i;
|
||||
}
|
||||
} else {
|
||||
// An external redirect doesn't contain RSC data.
|
||||
actionResult = undefined;
|
||||
actionFlightData = undefined;
|
||||
actionFlightDataRenderedSearch = undefined;
|
||||
actionFlightDataCouldBeIntercepted = undefined;
|
||||
}
|
||||
return {
|
||||
actionResult,
|
||||
actionFlightData,
|
||||
actionFlightDataRenderedSearch,
|
||||
actionFlightDataCouldBeIntercepted,
|
||||
redirectLocation,
|
||||
redirectType,
|
||||
revalidationKind,
|
||||
isPrerender
|
||||
};
|
||||
}
|
||||
/*
|
||||
* 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;
|
||||
const mutable = {};
|
||||
mutable.preserveCustomHistoryState = false;
|
||||
// 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, actionFlightDataCouldBeIntercepted: flightDataCouldBeIntercepted, redirectLocation, redirectType })=>{
|
||||
if (revalidationKind !== ActionDidNotRevalidate) {
|
||||
// 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 entire prefetch cache.
|
||||
// TODO: Evict only segments with matching tags and/or paths.
|
||||
if (revalidationKind === ActionDidRevalidateStaticAndDynamic) {
|
||||
revalidateEntireCache(nextUrl, state.tree);
|
||||
}
|
||||
}
|
||||
const pendingPush = redirectType !== RedirectType.replace;
|
||||
state.pushRef.pendingPush = pendingPush;
|
||||
mutable.pendingPush = pendingPush;
|
||||
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.
|
||||
const resolvedRedirectType = redirectType || RedirectType.push;
|
||||
if (isExternalURL(redirectLocation)) {
|
||||
// External redirect. Triggers an MPA navigation.
|
||||
const redirectHref = redirectLocation.href;
|
||||
const redirectError = createRedirectErrorForAction(redirectHref, resolvedRedirectType);
|
||||
reject(redirectError);
|
||||
return handleExternalUrl(state, mutable, redirectHref, pendingPush);
|
||||
} else {
|
||||
// Internal redirect. Triggers an SPA navigation.
|
||||
const redirectWithBasepath = createHrefFromUrl(redirectLocation, false);
|
||||
const redirectHref = hasBasePath(redirectWithBasepath) ? removeBasePath(redirectWithBasepath) : redirectWithBasepath;
|
||||
const redirectError = createRedirectErrorForAction(redirectHref, resolvedRedirectType);
|
||||
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 handleExternalUrl(state, mutable, redirectLocation.href, pendingPush);
|
||||
}
|
||||
if (typeof flightData === 'string') {
|
||||
// If the flight data is just a string, something earlier in the
|
||||
// response handling triggered an external redirect.
|
||||
return handleExternalUrl(state, mutable, flightData, pendingPush);
|
||||
}
|
||||
// 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 redirectUrl = redirectLocation !== undefined ? redirectLocation : currentUrl;
|
||||
const currentFlightRouterState = state.tree;
|
||||
const shouldScroll = true;
|
||||
// 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.
|
||||
if (flightData !== undefined) {
|
||||
const normalizedFlightData = flightData[0];
|
||||
if (normalizedFlightData !== undefined && // 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.
|
||||
normalizedFlightData.isRootRender && flightDataRenderedSearch !== undefined && flightDataCouldBeIntercepted !== 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 navigationSeed = {
|
||||
tree: normalizedFlightData.tree,
|
||||
renderedSearch: flightDataRenderedSearch,
|
||||
data: normalizedFlightData.seedData,
|
||||
head: normalizedFlightData.head
|
||||
};
|
||||
const now = Date.now();
|
||||
const result = navigateToSeededRoute(now, redirectUrl, redirectCanonicalUrl, navigationSeed, currentUrl, state.cache, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll);
|
||||
return handleNavigationResult(redirectUrl, state, mutable, pendingPush, result);
|
||||
}
|
||||
}
|
||||
// The server did not send back new data. We'll perform a regular, non-
|
||||
// seeded navigation — effectively the same as <Link> or router.push().
|
||||
const result = navigateUsingSegmentCache(redirectUrl, currentUrl, state.cache, currentFlightRouterState, nextUrl, freshnessPolicy, shouldScroll, mutable);
|
||||
return handleNavigationResult(redirectUrl, state, mutable, pendingPush, result);
|
||||
}, (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
+40
@@ -0,0 +1,40 @@
|
||||
import { createHrefFromUrl } from '../create-href-from-url';
|
||||
import { handleExternalUrl, handleNavigationResult } from './navigate-reducer';
|
||||
import { navigateToSeededRoute } from '../../segment-cache/navigation';
|
||||
import { refreshReducer } from './refresh-reducer';
|
||||
import { FreshnessPolicy } from '../ppr-navigations';
|
||||
export function serverPatchReducer(state, action) {
|
||||
const mutable = {};
|
||||
mutable.preserveCustomHistoryState = false;
|
||||
// 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;
|
||||
if (retryMpa || retrySeed === null) {
|
||||
// If the server did not send back data during the mismatch, fall back to
|
||||
// an MPA navigation.
|
||||
return handleExternalUrl(state, mutable, retryUrl.href, false);
|
||||
}
|
||||
const currentUrl = new URL(state.canonicalUrl, location.origin);
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
// A retry should not create a new history entry.
|
||||
const pendingPush = false;
|
||||
const shouldScroll = true;
|
||||
const now = Date.now();
|
||||
const result = navigateToSeededRoute(now, retryUrl, retryCanonicalUrl, retrySeed, currentUrl, state.cache, state.tree, FreshnessPolicy.RefreshAll, retryNextUrl, shouldScroll);
|
||||
return handleNavigationResult(retryUrl, state, mutable, pendingPush, result);
|
||||
}
|
||||
|
||||
//# 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 type {\n ServerPatchAction,\n ReducerState,\n ReadonlyReducerState,\n Mutable,\n} from '../router-reducer-types'\nimport { handleExternalUrl, handleNavigationResult } from './navigate-reducer'\nimport { navigateToSeededRoute } 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 const mutable: Mutable = {}\n mutable.preserveCustomHistoryState = false\n\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 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 handleExternalUrl(state, mutable, retryUrl.href, false)\n }\n const currentUrl = new URL(state.canonicalUrl, location.origin)\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)\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 // A retry should not create a new history entry.\n const pendingPush = false\n const shouldScroll = true\n const now = Date.now()\n const result = navigateToSeededRoute(\n now,\n retryUrl,\n retryCanonicalUrl,\n retrySeed,\n currentUrl,\n state.cache,\n state.tree,\n FreshnessPolicy.RefreshAll,\n retryNextUrl,\n shouldScroll\n )\n return handleNavigationResult(retryUrl, state, mutable, pendingPush, result)\n}\n"],"names":["createHrefFromUrl","handleExternalUrl","handleNavigationResult","navigateToSeededRoute","refreshReducer","FreshnessPolicy","serverPatchReducer","state","action","mutable","preserveCustomHistoryState","retryMpa","mpa","retryUrl","URL","url","location","origin","retrySeed","seed","href","currentUrl","canonicalUrl","previousTree","tree","retryCanonicalUrl","retryNextUrl","nextUrl","pendingPush","shouldScroll","now","Date","result","cache","RefreshAll"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,0BAAyB;AAO3D,SAASC,iBAAiB,EAAEC,sBAAsB,QAAQ,qBAAoB;AAC9E,SAASC,qBAAqB,QAAQ,iCAAgC;AACtE,SAASC,cAAc,QAAQ,oBAAmB;AAClD,SAASC,eAAe,QAAQ,qBAAoB;AAEpD,OAAO,SAASC,mBACdC,KAA2B,EAC3BC,MAAyB;IAEzB,MAAMC,UAAmB,CAAC;IAC1BA,QAAQC,0BAA0B,GAAG;IAErC,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,8BAA8B;IAC9B,MAAMC,WAAWH,OAAOI,GAAG;IAC3B,MAAMC,WAAW,IAAIC,IAAIN,OAAOO,GAAG,EAAEC,SAASC,MAAM;IACpD,MAAMC,YAAYV,OAAOW,IAAI;IAC7B,IAAIR,YAAYO,cAAc,MAAM;QAClC,yEAAyE;QACzE,qBAAqB;QACrB,OAAOjB,kBAAkBM,OAAOE,SAASI,SAASO,IAAI,EAAE;IAC1D;IACA,MAAMC,aAAa,IAAIP,IAAIP,MAAMe,YAAY,EAAEN,SAASC,MAAM;IAC9D,IAAIT,OAAOe,YAAY,KAAKhB,MAAMiB,IAAI,EAAE;QACtC,gEAAgE;QAChE,uEAAuE;QACvE,wCAAwC;QACxC,OAAOpB,eAAeG;IACxB;IACA,wEAAwE;IACxE,mDAAmD;IACnD,MAAMkB,oBAAoBzB,kBAAkBa;IAC5C,MAAMa,eAAelB,OAAOmB,OAAO;IACnC,iDAAiD;IACjD,MAAMC,cAAc;IACpB,MAAMC,eAAe;IACrB,MAAMC,MAAMC,KAAKD,GAAG;IACpB,MAAME,SAAS7B,sBACb2B,KACAjB,UACAY,mBACAP,WACAG,YACAd,MAAM0B,KAAK,EACX1B,MAAMiB,IAAI,EACVnB,gBAAgB6B,UAAU,EAC1BR,cACAG;IAEF,OAAO3B,uBAAuBW,UAAUN,OAAOE,SAASmB,aAAaI;AACvE","ignoreList":[0]}
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}({});
|
||||
|
||||
//# 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);
|
||||
}
|
||||
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)\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;YACxB;QACA,KAAKT;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