inital commit

This commit is contained in:
2026-01-01 15:25:19 +05:30
commit f0ae49465a
36361 changed files with 4894111 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
import type { FlightRouterState } from '../../../server/app-render/types';
import type { NormalizedFlightData } from '../../flight-data-helpers';
import type { Mutable, ReadonlyReducerState } from './router-reducer-types';
/**
* This is a stop-gap until per-segment caching is implemented. It leverages the `aliased` flag that is added
* to prefetch entries when it's determined that the loading state from that entry should be used for this navigation.
* This function takes the aliased entry and only applies the loading state to the updated cache node.
* We should remove this once per-segment fetching is implemented as ideally the prefetch cache will contain a
* more granular segment map and so the router will be able to simply re-use the loading segment for the new navigation.
*/
export declare function handleAliasedPrefetchEntry(state: ReadonlyReducerState, flightData: string | NormalizedFlightData[], url: URL, mutable: Mutable): false | import("./router-reducer-types").ReducerState;
/**
* Add search params to the page segments in the flight router state
* Page segments that are associated with search params have a page segment key
* followed by a query string. This function will add those params to the page segment.
* This is useful if we return an aliased prefetch entry (ie, won't have search params)
* but the canonical router URL has search params.
*/
export declare function addSearchParamsToPageSegments(flightRouterState: FlightRouterState, searchParams: Record<string, string | string[] | undefined>): FlightRouterState;

View File

@@ -0,0 +1,191 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
addSearchParamsToPageSegments: null,
handleAliasedPrefetchEntry: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
addSearchParamsToPageSegments: function() {
return addSearchParamsToPageSegments;
},
handleAliasedPrefetchEntry: function() {
return handleAliasedPrefetchEntry;
}
});
const _segment = require("../../../shared/lib/segment");
const _approuter = require("../app-router");
const _applyrouterstatepatchtotree = require("./apply-router-state-patch-to-tree");
const _createhreffromurl = require("./create-href-from-url");
const _createroutercachekey = require("./create-router-cache-key");
const _fillcachewithnewsubtreedata = require("./fill-cache-with-new-subtree-data");
const _handlemutable = require("./handle-mutable");
function handleAliasedPrefetchEntry(state, flightData, url, mutable) {
let currentTree = state.tree;
let currentCache = state.cache;
const href = (0, _createhreffromurl.createHrefFromUrl)(url);
let applied;
if (typeof flightData === 'string') {
return false;
}
for (const normalizedFlightData of flightData){
// If the segment doesn't have a loading component, we don't need to do anything.
if (!hasLoadingComponentInSeedData(normalizedFlightData.seedData)) {
continue;
}
let treePatch = normalizedFlightData.tree;
// Segments are keyed by searchParams (e.g. __PAGE__?{"foo":"bar"}). We might return a less specific, param-less entry,
// so we ensure that the final tree contains the correct searchParams (reflected in the URL) are provided in the updated FlightRouterState tree.
// We only do this on the first read, as otherwise we'd be overwriting the searchParams that may have already been set
treePatch = addSearchParamsToPageSegments(treePatch, Object.fromEntries(url.searchParams));
const { seedData, isRootRender, pathToSegment } = normalizedFlightData;
// TODO-APP: remove ''
const flightSegmentPathWithLeadingEmpty = [
'',
...pathToSegment
];
// Segments are keyed by searchParams (e.g. __PAGE__?{"foo":"bar"}). We might return a less specific, param-less entry,
// so we ensure that the final tree contains the correct searchParams (reflected in the URL) are provided in the updated FlightRouterState tree.
// We only do this on the first read, as otherwise we'd be overwriting the searchParams that may have already been set
treePatch = addSearchParamsToPageSegments(treePatch, Object.fromEntries(url.searchParams));
let newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(flightSegmentPathWithLeadingEmpty, currentTree, treePatch, href);
const newCache = (0, _approuter.createEmptyCacheNode)();
// The prefetch cache entry was aliased -- this signals that we only fill in the cache with the
// loading state and not the actual parallel route seed data.
if (isRootRender && seedData) {
// Fill in the cache with the new loading / rsc data
const rsc = seedData[1];
const loading = seedData[3];
newCache.loading = loading;
newCache.rsc = rsc;
// Construct a new tree and apply the aliased loading state for each parallel route
fillNewTreeWithOnlyLoadingSegments(newCache, currentCache, treePatch, seedData);
} else {
// Copy rsc for the root node of the cache.
newCache.rsc = currentCache.rsc;
newCache.prefetchRsc = currentCache.prefetchRsc;
newCache.loading = currentCache.loading;
newCache.parallelRoutes = new Map(currentCache.parallelRoutes);
// copy the loading state only into the leaf node (the part that changed)
(0, _fillcachewithnewsubtreedata.fillCacheWithNewSubTreeDataButOnlyLoading)(newCache, currentCache, normalizedFlightData);
}
// If we don't have an updated tree, there's no reason to update the cache, as the tree
// dictates what cache nodes to render.
if (newTree) {
currentTree = newTree;
currentCache = newCache;
applied = true;
}
}
if (!applied) {
return false;
}
mutable.patchedTree = currentTree;
mutable.cache = currentCache;
mutable.canonicalUrl = href;
mutable.hashFragment = url.hash;
return (0, _handlemutable.handleMutable)(state, mutable);
}
function hasLoadingComponentInSeedData(seedData) {
if (!seedData) return false;
const parallelRoutes = seedData[2];
const loading = seedData[3];
if (loading) {
return true;
}
for(const key in parallelRoutes){
if (hasLoadingComponentInSeedData(parallelRoutes[key])) {
return true;
}
}
return false;
}
function fillNewTreeWithOnlyLoadingSegments(newCache, existingCache, routerState, cacheNodeSeedData) {
const isLastSegment = Object.keys(routerState[1]).length === 0;
if (isLastSegment) {
return;
}
for(const key in routerState[1]){
const parallelRouteState = routerState[1][key];
const segmentForParallelRoute = parallelRouteState[0];
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segmentForParallelRoute);
const parallelSeedData = cacheNodeSeedData !== null && cacheNodeSeedData[2][key] !== undefined ? cacheNodeSeedData[2][key] : null;
let newCacheNode;
if (parallelSeedData !== null) {
// New data was sent from the server.
const rsc = parallelSeedData[1];
const loading = parallelSeedData[3];
newCacheNode = {
lazyData: null,
// copy the layout but null the page segment as that's not meant to be used
rsc: segmentForParallelRoute.includes(_segment.PAGE_SEGMENT_KEY) ? null : rsc,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(),
loading
};
} else {
// No data available for this node. This will trigger a lazy fetch
// during render.
newCacheNode = {
lazyData: null,
rsc: null,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(),
loading: null
};
}
const existingParallelRoutes = newCache.parallelRoutes.get(key);
if (existingParallelRoutes) {
existingParallelRoutes.set(cacheKey, newCacheNode);
} else {
newCache.parallelRoutes.set(key, new Map([
[
cacheKey,
newCacheNode
]
]));
}
fillNewTreeWithOnlyLoadingSegments(newCacheNode, existingCache, parallelRouteState, parallelSeedData);
}
}
function addSearchParamsToPageSegments(flightRouterState, searchParams) {
const [segment, parallelRoutes, ...rest] = flightRouterState;
// If it's a page segment, modify the segment by adding search params
if (segment.includes(_segment.PAGE_SEGMENT_KEY)) {
const newSegment = (0, _segment.addSearchParamsIfPageSegment)(segment, searchParams);
return [
newSegment,
parallelRoutes,
...rest
];
}
// Otherwise, recurse through the parallel routes and return a new tree
const updatedParallelRoutes = {};
for (const [key, parallelRoute] of Object.entries(parallelRoutes)){
updatedParallelRoutes[key] = addSearchParamsToPageSegments(parallelRoute, searchParams);
}
return [
segment,
updatedParallelRoutes,
...rest
];
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=aliased-prefetch-navigations.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { PrefetchCacheEntry } from './router-reducer-types';
import type { NormalizedFlightData } from '../../flight-data-helpers';
export declare function applyFlightData(existingCache: CacheNode, cache: CacheNode, flightData: NormalizedFlightData, prefetchEntry?: PrefetchCacheEntry): boolean;

View File

@@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "applyFlightData", {
enumerable: true,
get: function() {
return applyFlightData;
}
});
const _filllazyitemstillleafwithhead = require("./fill-lazy-items-till-leaf-with-head");
const _fillcachewithnewsubtreedata = require("./fill-cache-with-new-subtree-data");
function applyFlightData(existingCache, cache, flightData, prefetchEntry) {
// The one before last item is the router state tree patch
const { tree: treePatch, seedData, head, isRootRender } = flightData;
// Handles case where prefetch only returns the router tree patch without rendered components.
if (seedData === null) {
return false;
}
if (isRootRender) {
const rsc = seedData[1];
const loading = seedData[3];
cache.loading = loading;
cache.rsc = rsc;
// This is a PPR-only field. When PPR is enabled, we shouldn't hit
// this path during a navigation, but until PPR is fully implemented
// yet it's possible the existing node does have a non-null
// `prefetchRsc`. As an incremental step, we'll just de-opt to the
// old behavior — no PPR value.
cache.prefetchRsc = null;
(0, _filllazyitemstillleafwithhead.fillLazyItemsTillLeafWithHead)(cache, existingCache, treePatch, seedData, head, prefetchEntry);
} else {
// Copy rsc for the root node of the cache.
cache.rsc = existingCache.rsc;
// This is a PPR-only field. Unlike the previous branch, since we're
// just cloning the existing cache node, we might as well keep the
// PPR value, if it exists.
cache.prefetchRsc = existingCache.prefetchRsc;
cache.parallelRoutes = new Map(existingCache.parallelRoutes);
cache.loading = existingCache.loading;
// Create a copy of the existing cache with the rsc applied.
(0, _fillcachewithnewsubtreedata.fillCacheWithNewSubTreeData)(cache, existingCache, flightData, prefetchEntry);
}
return true;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=apply-flight-data.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/apply-flight-data.ts"],"sourcesContent":["import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'\nimport { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head'\nimport { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data'\nimport type { PrefetchCacheEntry } from './router-reducer-types'\nimport type { NormalizedFlightData } from '../../flight-data-helpers'\n\nexport function applyFlightData(\n existingCache: CacheNode,\n cache: CacheNode,\n flightData: NormalizedFlightData,\n prefetchEntry?: PrefetchCacheEntry\n): boolean {\n // The one before last item is the router state tree patch\n const { tree: treePatch, seedData, head, isRootRender } = flightData\n\n // Handles case where prefetch only returns the router tree patch without rendered components.\n if (seedData === null) {\n return false\n }\n\n if (isRootRender) {\n const rsc = seedData[1]\n const loading = seedData[3]\n cache.loading = loading\n cache.rsc = rsc\n // This is a PPR-only field. When PPR is enabled, we shouldn't hit\n // this path during a navigation, but until PPR is fully implemented\n // yet it's possible the existing node does have a non-null\n // `prefetchRsc`. As an incremental step, we'll just de-opt to the\n // old behavior — no PPR value.\n cache.prefetchRsc = null\n fillLazyItemsTillLeafWithHead(\n cache,\n existingCache,\n treePatch,\n seedData,\n head,\n prefetchEntry\n )\n } else {\n // Copy rsc for the root node of the cache.\n cache.rsc = existingCache.rsc\n // This is a PPR-only field. Unlike the previous branch, since we're\n // just cloning the existing cache node, we might as well keep the\n // PPR value, if it exists.\n cache.prefetchRsc = existingCache.prefetchRsc\n cache.parallelRoutes = new Map(existingCache.parallelRoutes)\n cache.loading = existingCache.loading\n // Create a copy of the existing cache with the rsc applied.\n fillCacheWithNewSubTreeData(cache, existingCache, flightData, prefetchEntry)\n }\n\n return true\n}\n"],"names":["applyFlightData","existingCache","cache","flightData","prefetchEntry","tree","treePatch","seedData","head","isRootRender","rsc","loading","prefetchRsc","fillLazyItemsTillLeafWithHead","parallelRoutes","Map","fillCacheWithNewSubTreeData"],"mappings":";;;;+BAMgBA;;;eAAAA;;;+CAL8B;6CACF;AAIrC,SAASA,gBACdC,aAAwB,EACxBC,KAAgB,EAChBC,UAAgC,EAChCC,aAAkC;IAElC,0DAA0D;IAC1D,MAAM,EAAEC,MAAMC,SAAS,EAAEC,QAAQ,EAAEC,IAAI,EAAEC,YAAY,EAAE,GAAGN;IAE1D,8FAA8F;IAC9F,IAAII,aAAa,MAAM;QACrB,OAAO;IACT;IAEA,IAAIE,cAAc;QAChB,MAAMC,MAAMH,QAAQ,CAAC,EAAE;QACvB,MAAMI,UAAUJ,QAAQ,CAAC,EAAE;QAC3BL,MAAMS,OAAO,GAAGA;QAChBT,MAAMQ,GAAG,GAAGA;QACZ,kEAAkE;QAClE,oEAAoE;QACpE,2DAA2D;QAC3D,kEAAkE;QAClE,+BAA+B;QAC/BR,MAAMU,WAAW,GAAG;QACpBC,IAAAA,4DAA6B,EAC3BX,OACAD,eACAK,WACAC,UACAC,MACAJ;IAEJ,OAAO;QACL,2CAA2C;QAC3CF,MAAMQ,GAAG,GAAGT,cAAcS,GAAG;QAC7B,oEAAoE;QACpE,kEAAkE;QAClE,2BAA2B;QAC3BR,MAAMU,WAAW,GAAGX,cAAcW,WAAW;QAC7CV,MAAMY,cAAc,GAAG,IAAIC,IAAId,cAAca,cAAc;QAC3DZ,MAAMS,OAAO,GAAGV,cAAcU,OAAO;QACrC,4DAA4D;QAC5DK,IAAAA,wDAA2B,EAACd,OAAOD,eAAeE,YAAYC;IAChE;IAEA,OAAO;AACT"}

View File

@@ -0,0 +1,7 @@
import type { FlightRouterState, FlightSegmentPath } from '../../../server/app-render/types';
/**
* Apply the router state from the Flight response, but skip patching default segments.
* Useful for patching the router cache when navigating, where we persist the existing default segment if there isn't a new one.
* Creates a new router state tree.
*/
export declare function applyRouterStatePatchToTree(flightSegmentPath: FlightSegmentPath, flightRouterState: FlightRouterState, treePatch: FlightRouterState, path: string): FlightRouterState | null;

View File

@@ -0,0 +1,105 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "applyRouterStatePatchToTree", {
enumerable: true,
get: function() {
return applyRouterStatePatchToTree;
}
});
const _segment = require("../../../shared/lib/segment");
const _flightdatahelpers = require("../../flight-data-helpers");
const _matchsegments = require("../match-segments");
const _refetchinactiveparallelsegments = require("./refetch-inactive-parallel-segments");
/**
* Deep merge of the two router states. Parallel route keys are preserved if the patch doesn't have them.
*/ function applyPatch(initialTree, patchTree) {
const [initialSegment, initialParallelRoutes] = initialTree;
const [patchSegment, patchParallelRoutes] = patchTree;
// if the applied patch segment is __DEFAULT__ then it can be ignored in favor of the initial tree
// this is because the __DEFAULT__ segment is used as a placeholder on navigation
if (patchSegment === _segment.DEFAULT_SEGMENT_KEY && initialSegment !== _segment.DEFAULT_SEGMENT_KEY) {
return initialTree;
}
if ((0, _matchsegments.matchSegment)(initialSegment, patchSegment)) {
const newParallelRoutes = {};
for(const key in initialParallelRoutes){
const isInPatchTreeParallelRoutes = typeof patchParallelRoutes[key] !== 'undefined';
if (isInPatchTreeParallelRoutes) {
newParallelRoutes[key] = applyPatch(initialParallelRoutes[key], patchParallelRoutes[key]);
} else {
newParallelRoutes[key] = initialParallelRoutes[key];
}
}
for(const key in patchParallelRoutes){
if (newParallelRoutes[key]) {
continue;
}
newParallelRoutes[key] = patchParallelRoutes[key];
}
const tree = [
initialSegment,
newParallelRoutes
];
// Copy over the existing tree
if (initialTree[2]) {
tree[2] = initialTree[2];
}
if (initialTree[3]) {
tree[3] = initialTree[3];
}
if (initialTree[4]) {
tree[4] = initialTree[4];
}
return tree;
}
return patchTree;
}
function applyRouterStatePatchToTree(flightSegmentPath, flightRouterState, treePatch, path) {
const [segment, parallelRoutes, url, refetch, isRootLayout] = flightRouterState;
// Root refresh
if (flightSegmentPath.length === 1) {
const tree = applyPatch(flightRouterState, treePatch);
(0, _refetchinactiveparallelsegments.addRefreshMarkerToActiveParallelSegments)(tree, path);
return tree;
}
const [currentSegment, parallelRouteKey] = flightSegmentPath;
// Tree path returned from the server should always match up with the current tree in the browser
if (!(0, _matchsegments.matchSegment)(currentSegment, segment)) {
return null;
}
const lastSegment = flightSegmentPath.length === 2;
let parallelRoutePatch;
if (lastSegment) {
parallelRoutePatch = applyPatch(parallelRoutes[parallelRouteKey], treePatch);
} else {
parallelRoutePatch = applyRouterStatePatchToTree((0, _flightdatahelpers.getNextFlightSegmentPath)(flightSegmentPath), parallelRoutes[parallelRouteKey], treePatch, path);
if (parallelRoutePatch === null) {
return null;
}
}
const tree = [
flightSegmentPath[0],
{
...parallelRoutes,
[parallelRouteKey]: parallelRoutePatch
},
url,
refetch
];
// Current segment is the root layout
if (isRootLayout) {
tree[4] = true;
}
(0, _refetchinactiveparallelsegments.addRefreshMarkerToActiveParallelSegments)(tree, path);
return tree;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=apply-router-state-patch-to-tree.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
import type { FlightSegmentPath } from '../../../server/app-render/types';
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
/**
* This will clear the CacheNode data for a particular segment path. This will cause a lazy-fetch in layout router to fill in new data.
*/
export declare function clearCacheNodeDataForSegmentPath(newCache: CacheNode, existingCache: CacheNode, flightSegmentPath: FlightSegmentPath): void;

View File

@@ -0,0 +1,76 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "clearCacheNodeDataForSegmentPath", {
enumerable: true,
get: function() {
return clearCacheNodeDataForSegmentPath;
}
});
const _flightdatahelpers = require("../../flight-data-helpers");
const _createroutercachekey = require("./create-router-cache-key");
function clearCacheNodeDataForSegmentPath(newCache, existingCache, flightSegmentPath) {
const isLastEntry = flightSegmentPath.length <= 2;
const [parallelRouteKey, segment] = flightSegmentPath;
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segment);
const existingChildSegmentMap = existingCache.parallelRoutes.get(parallelRouteKey);
let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey);
if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
childSegmentMap = new Map(existingChildSegmentMap);
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap);
}
const existingChildCacheNode = existingChildSegmentMap == null ? void 0 : existingChildSegmentMap.get(cacheKey);
let childCacheNode = childSegmentMap.get(cacheKey);
// In case of last segment start off the fetch at this level and don't copy further down.
if (isLastEntry) {
if (!childCacheNode || !childCacheNode.lazyData || childCacheNode === existingChildCacheNode) {
childSegmentMap.set(cacheKey, {
lazyData: null,
rsc: null,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(),
loading: null
});
}
return;
}
if (!childCacheNode || !existingChildCacheNode) {
// Start fetch in the place where the existing cache doesn't have the data yet.
if (!childCacheNode) {
childSegmentMap.set(cacheKey, {
lazyData: null,
rsc: null,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(),
loading: null
});
}
return;
}
if (childCacheNode === existingChildCacheNode) {
childCacheNode = {
lazyData: childCacheNode.lazyData,
rsc: childCacheNode.rsc,
prefetchRsc: childCacheNode.prefetchRsc,
head: childCacheNode.head,
prefetchHead: childCacheNode.prefetchHead,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
loading: childCacheNode.loading
};
childSegmentMap.set(cacheKey, childCacheNode);
}
return clearCacheNodeDataForSegmentPath(childCacheNode, existingChildCacheNode, (0, _flightdatahelpers.getNextFlightSegmentPath)(flightSegmentPath));
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=clear-cache-node-data-for-segment-path.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/clear-cache-node-data-for-segment-path.ts"],"sourcesContent":["import type { FlightSegmentPath } from '../../../server/app-render/types'\nimport type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'\nimport { getNextFlightSegmentPath } from '../../flight-data-helpers'\nimport { createRouterCacheKey } from './create-router-cache-key'\n\n/**\n * This will clear the CacheNode data for a particular segment path. This will cause a lazy-fetch in layout router to fill in new data.\n */\nexport function clearCacheNodeDataForSegmentPath(\n newCache: CacheNode,\n existingCache: CacheNode,\n flightSegmentPath: FlightSegmentPath\n): void {\n const isLastEntry = flightSegmentPath.length <= 2\n\n const [parallelRouteKey, segment] = flightSegmentPath\n const cacheKey = createRouterCacheKey(segment)\n\n const existingChildSegmentMap =\n existingCache.parallelRoutes.get(parallelRouteKey)\n\n let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey)\n\n if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {\n childSegmentMap = new Map(existingChildSegmentMap)\n newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap)\n }\n\n const existingChildCacheNode = existingChildSegmentMap?.get(cacheKey)\n let childCacheNode = childSegmentMap.get(cacheKey)\n\n // In case of last segment start off the fetch at this level and don't copy further down.\n if (isLastEntry) {\n if (\n !childCacheNode ||\n !childCacheNode.lazyData ||\n childCacheNode === existingChildCacheNode\n ) {\n childSegmentMap.set(cacheKey, {\n lazyData: null,\n rsc: null,\n prefetchRsc: null,\n head: null,\n prefetchHead: null,\n parallelRoutes: new Map(),\n loading: null,\n })\n }\n return\n }\n\n if (!childCacheNode || !existingChildCacheNode) {\n // Start fetch in the place where the existing cache doesn't have the data yet.\n if (!childCacheNode) {\n childSegmentMap.set(cacheKey, {\n lazyData: null,\n rsc: null,\n prefetchRsc: null,\n head: null,\n prefetchHead: null,\n parallelRoutes: new Map(),\n loading: null,\n })\n }\n return\n }\n\n if (childCacheNode === existingChildCacheNode) {\n childCacheNode = {\n lazyData: childCacheNode.lazyData,\n rsc: childCacheNode.rsc,\n prefetchRsc: childCacheNode.prefetchRsc,\n head: childCacheNode.head,\n prefetchHead: childCacheNode.prefetchHead,\n parallelRoutes: new Map(childCacheNode.parallelRoutes),\n loading: childCacheNode.loading,\n } as CacheNode\n childSegmentMap.set(cacheKey, childCacheNode)\n }\n\n return clearCacheNodeDataForSegmentPath(\n childCacheNode,\n existingChildCacheNode,\n getNextFlightSegmentPath(flightSegmentPath)\n )\n}\n"],"names":["clearCacheNodeDataForSegmentPath","newCache","existingCache","flightSegmentPath","isLastEntry","length","parallelRouteKey","segment","cacheKey","createRouterCacheKey","existingChildSegmentMap","parallelRoutes","get","childSegmentMap","Map","set","existingChildCacheNode","childCacheNode","lazyData","rsc","prefetchRsc","head","prefetchHead","loading","getNextFlightSegmentPath"],"mappings":";;;;+BAQgBA;;;eAAAA;;;mCANyB;sCACJ;AAK9B,SAASA,iCACdC,QAAmB,EACnBC,aAAwB,EACxBC,iBAAoC;IAEpC,MAAMC,cAAcD,kBAAkBE,MAAM,IAAI;IAEhD,MAAM,CAACC,kBAAkBC,QAAQ,GAAGJ;IACpC,MAAMK,WAAWC,IAAAA,0CAAoB,EAACF;IAEtC,MAAMG,0BACJR,cAAcS,cAAc,CAACC,GAAG,CAACN;IAEnC,IAAIO,kBAAkBZ,SAASU,cAAc,CAACC,GAAG,CAACN;IAElD,IAAI,CAACO,mBAAmBA,oBAAoBH,yBAAyB;QACnEG,kBAAkB,IAAIC,IAAIJ;QAC1BT,SAASU,cAAc,CAACI,GAAG,CAACT,kBAAkBO;IAChD;IAEA,MAAMG,yBAAyBN,2CAAAA,wBAAyBE,GAAG,CAACJ;IAC5D,IAAIS,iBAAiBJ,gBAAgBD,GAAG,CAACJ;IAEzC,yFAAyF;IACzF,IAAIJ,aAAa;QACf,IACE,CAACa,kBACD,CAACA,eAAeC,QAAQ,IACxBD,mBAAmBD,wBACnB;YACAH,gBAAgBE,GAAG,CAACP,UAAU;gBAC5BU,UAAU;gBACVC,KAAK;gBACLC,aAAa;gBACbC,MAAM;gBACNC,cAAc;gBACdX,gBAAgB,IAAIG;gBACpBS,SAAS;YACX;QACF;QACA;IACF;IAEA,IAAI,CAACN,kBAAkB,CAACD,wBAAwB;QAC9C,+EAA+E;QAC/E,IAAI,CAACC,gBAAgB;YACnBJ,gBAAgBE,GAAG,CAACP,UAAU;gBAC5BU,UAAU;gBACVC,KAAK;gBACLC,aAAa;gBACbC,MAAM;gBACNC,cAAc;gBACdX,gBAAgB,IAAIG;gBACpBS,SAAS;YACX;QACF;QACA;IACF;IAEA,IAAIN,mBAAmBD,wBAAwB;QAC7CC,iBAAiB;YACfC,UAAUD,eAAeC,QAAQ;YACjCC,KAAKF,eAAeE,GAAG;YACvBC,aAAaH,eAAeG,WAAW;YACvCC,MAAMJ,eAAeI,IAAI;YACzBC,cAAcL,eAAeK,YAAY;YACzCX,gBAAgB,IAAIG,IAAIG,eAAeN,cAAc;YACrDY,SAASN,eAAeM,OAAO;QACjC;QACAV,gBAAgBE,GAAG,CAACP,UAAUS;IAChC;IAEA,OAAOjB,iCACLiB,gBACAD,wBACAQ,IAAAA,2CAAwB,EAACrB;AAE7B"}

View File

@@ -0,0 +1,8 @@
import type { FlightRouterState } from '../../../server/app-render/types';
import type { Params } from '../../../server/request/params';
export declare function extractPathFromFlightRouterState(flightRouterState: FlightRouterState): string | undefined;
export declare function computeChangedPath(treeA: FlightRouterState, treeB: FlightRouterState): string | null;
/**
* Recursively extracts dynamic parameters from FlightRouterState.
*/
export declare function getSelectedParams(currentTree: FlightRouterState, params?: Params): Params;

View File

@@ -0,0 +1,131 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
computeChangedPath: null,
extractPathFromFlightRouterState: null,
getSelectedParams: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
computeChangedPath: function() {
return computeChangedPath;
},
extractPathFromFlightRouterState: function() {
return extractPathFromFlightRouterState;
},
getSelectedParams: function() {
return getSelectedParams;
}
});
const _interceptionroutes = require("../../../shared/lib/router/utils/interception-routes");
const _segment = require("../../../shared/lib/segment");
const _matchsegments = require("../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 === '' || (0, _segment.isGroupSegment)(segment)) {
return acc;
}
return acc + "/" + segment;
}, '') || '/';
}
function extractPathFromFlightRouterState(flightRouterState) {
const segment = Array.isArray(flightRouterState[0]) ? flightRouterState[0][1] : flightRouterState[0];
if (segment === _segment.DEFAULT_SEGMENT_KEY || _interceptionroutes.INTERCEPTION_ROUTE_MARKERS.some((m)=>segment.startsWith(m))) return undefined;
if (segment.startsWith(_segment.PAGE_SEGMENT_KEY)) return '';
const segments = [
segmentToPathname(segment)
];
var _flightRouterState_;
const parallelRoutes = (_flightRouterState_ = flightRouterState[1]) != null ? _flightRouterState_ : {};
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 (_interceptionroutes.INTERCEPTION_ROUTE_MARKERS.some((m)=>normalizedSegmentA.startsWith(m) || normalizedSegmentB.startsWith(m))) {
return '';
}
if (!(0, _matchsegments.matchSegment)(segmentA, segmentB)) {
var _extractPathFromFlightRouterState;
// once we find where the tree changed, we compute the rest of the path by traversing the tree
return (_extractPathFromFlightRouterState = extractPathFromFlightRouterState(treeB)) != null ? _extractPathFromFlightRouterState : '';
}
for(const parallelRouterKey in parallelRoutesA){
if (parallelRoutesB[parallelRouterKey]) {
const changedPath = computeChangedPathImpl(parallelRoutesA[parallelRouterKey], parallelRoutesB[parallelRouterKey]);
if (changedPath !== null) {
return segmentToPathname(segmentB) + "/" + changedPath;
}
}
}
return null;
}
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('/'));
}
function getSelectedParams(currentTree, params) {
if (params === void 0) 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(_segment.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;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=compute-changed-path.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export declare function createHrefFromUrl(url: Pick<URL, 'pathname' | 'search' | 'hash'>, includeHash?: boolean): string;

View File

@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "createHrefFromUrl", {
enumerable: true,
get: function() {
return createHrefFromUrl;
}
});
function createHrefFromUrl(url, includeHash) {
if (includeHash === void 0) includeHash = true;
return url.pathname + url.search + (includeHash ? url.hash : '');
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=create-href-from-url.js.map

View File

@@ -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":";;;;+BAAgBA;;;eAAAA;;;AAAT,SAASA,kBACdC,GAA8C,EAC9CC,WAA2B;IAA3BA,IAAAA,wBAAAA,cAAuB;IAEvB,OAAOD,IAAIE,QAAQ,GAAGF,IAAIG,MAAM,GAAIF,CAAAA,cAAcD,IAAII,IAAI,GAAG,EAAC;AAChE"}

View File

@@ -0,0 +1,30 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { FlightDataPath } from '../../../server/app-render/types';
import { type PrefetchCacheEntry } from './router-reducer-types';
export interface InitialRouterStateParameters {
initialCanonicalUrlParts: string[];
initialParallelRoutes: CacheNode['parallelRoutes'];
initialFlightData: FlightDataPath[];
location: Location | null;
couldBeIntercepted: boolean;
postponed: boolean;
prerendered: boolean;
}
export declare function createInitialRouterState({ initialFlightData, initialCanonicalUrlParts, initialParallelRoutes, location, couldBeIntercepted, postponed, prerendered, }: InitialRouterStateParameters): {
tree: import("../../../server/app-render/types").FlightRouterState;
cache: import("../../../shared/lib/app-router-context.shared-runtime").ReadyCacheNode;
prefetchCache: Map<string, PrefetchCacheEntry>;
pushRef: {
pendingPush: boolean;
mpaNavigation: boolean;
preserveCustomHistoryState: boolean;
};
focusAndScrollRef: {
apply: boolean;
onlyHashChange: boolean;
hashFragment: null;
segmentPaths: never[];
};
canonicalUrl: string;
nextUrl: string | null;
};

View File

@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "createInitialRouterState", {
enumerable: true,
get: function() {
return createInitialRouterState;
}
});
const _createhreffromurl = require("./create-href-from-url");
const _filllazyitemstillleafwithhead = require("./fill-lazy-items-till-leaf-with-head");
const _computechangedpath = require("./compute-changed-path");
const _prefetchcacheutils = require("./prefetch-cache-utils");
const _routerreducertypes = require("./router-reducer-types");
const _refetchinactiveparallelsegments = require("./refetch-inactive-parallel-segments");
const _flightdatahelpers = require("../../flight-data-helpers");
function createInitialRouterState(param) {
let { initialFlightData, initialCanonicalUrlParts, initialParallelRoutes, location, couldBeIntercepted, postponed, prerendered } = param;
// 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 = (0, _flightdatahelpers.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 rsc = initialSeedData == null ? void 0 : initialSeedData[1];
var _initialSeedData_;
const loading = (_initialSeedData_ = initialSeedData == null ? void 0 : initialSeedData[3]) != null ? _initialSeedData_ : null;
const cache = {
lazyData: null,
rsc,
prefetchRsc: null,
head: null,
prefetchHead: null,
// The cache gets seeded during the first render. `initialParallelRoutes` ensures the cache from the first render is there during the second render.
parallelRoutes: initialParallelRoutes,
loading
};
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 ? (0, _createhreffromurl.createHrefFromUrl)(location) : initialCanonicalUrl;
(0, _refetchinactiveparallelsegments.addRefreshMarkerToActiveParallelSegments)(initialTree, canonicalUrl);
const prefetchCache = new Map();
// When the cache hasn't been seeded yet we fill the cache with the head.
if (initialParallelRoutes === null || initialParallelRoutes.size === 0) {
(0, _filllazyitemstillleafwithhead.fillLazyItemsTillLeafWithHead)(cache, undefined, initialTree, initialSeedData, initialHead, undefined);
}
var // the || operator is intentional, the pathname can be an empty string
_ref;
const initialState = {
tree: initialTree,
cache,
prefetchCache,
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,
nextUrl: (_ref = (0, _computechangedpath.extractPathFromFlightRouterState)(initialTree) || (location == null ? void 0 : location.pathname)) != null ? _ref : null
};
if (process.env.NODE_ENV !== 'development' && location) {
// Seed the prefetch cache with this page's data.
// This is to prevent needlessly re-prefetching a page that is already reusable,
// and will avoid triggering a loading state/data fetch stall when navigating back to the page.
// We don't currently do this in development because links aren't prefetched in development
// so having a mismatch between prefetch/no prefetch provides inconsistent behavior based on which page
// was loaded first.
const url = new URL("" + location.pathname + location.search, location.origin);
(0, _prefetchcacheutils.createSeededPrefetchCacheEntry)({
url,
data: {
flightData: [
normalizedFlightData
],
canonicalUrl: undefined,
couldBeIntercepted: !!couldBeIntercepted,
prerendered,
postponed,
// TODO: The initial RSC payload includes both static and dynamic data
// in the same response, even if PPR is enabled. So if there's any
// dynamic data at all, we can't set a stale time. In the future we may
// add a way to split a single Flight stream into static and dynamic
// parts. But in the meantime we should at least make this work for
// fully static pages.
staleTime: -1
},
tree: initialState.tree,
prefetchCache: initialState.prefetchCache,
nextUrl: initialState.nextUrl,
kind: prerendered ? _routerreducertypes.PrefetchKind.FULL : _routerreducertypes.PrefetchKind.AUTO
});
}
return initialState;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=create-initial-router-state.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
import type { Segment } from '../../../server/app-render/types';
export declare function createRouterCacheKey(segment: Segment, withoutSearchParameters?: boolean): string;

View File

@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "createRouterCacheKey", {
enumerable: true,
get: function() {
return createRouterCacheKey;
}
});
const _segment = require("../../../shared/lib/segment");
function createRouterCacheKey(segment, withoutSearchParameters) {
if (withoutSearchParameters === void 0) 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(_segment.PAGE_SEGMENT_KEY)) {
return _segment.PAGE_SEGMENT_KEY;
}
return segment;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=create-router-cache-key.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/create-router-cache-key.ts"],"sourcesContent":["import type { Segment } from '../../../server/app-render/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":["createRouterCacheKey","segment","withoutSearchParameters","Array","isArray","startsWith","PAGE_SEGMENT_KEY"],"mappings":";;;;+BAGgBA;;;eAAAA;;;yBAFiB;AAE1B,SAASA,qBACdC,OAAgB,EAChBC,uBAAwC;IAAxCA,IAAAA,oCAAAA,0BAAmC;IAEnC,8DAA8D;IAC9D,uGAAuG;IACvG,IAAIC,MAAMC,OAAO,CAACH,UAAU;QAC1B,OAAO,AAAGA,OAAO,CAAC,EAAE,GAAC,MAAGA,OAAO,CAAC,EAAE,GAAC,MAAGA,OAAO,CAAC,EAAE;IAClD;IAEA,kEAAkE;IAClE,kFAAkF;IAClF,IAAIC,2BAA2BD,QAAQI,UAAU,CAACC,yBAAgB,GAAG;QACnE,OAAOA,yBAAgB;IACzB;IAEA,OAAOL;AACT"}

View File

@@ -0,0 +1,37 @@
import type { FlightRouterState } from '../../../server/app-render/types';
import type { NEXT_ROUTER_SEGMENT_PREFETCH_HEADER } from '../app-router-headers';
import { NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_URL, RSC_HEADER, NEXT_HMR_REFRESH_HEADER } from '../app-router-headers';
import { PrefetchKind } from './router-reducer-types';
import { type NormalizedFlightData } from '../../flight-data-helpers';
export interface FetchServerResponseOptions {
readonly flightRouterState: FlightRouterState;
readonly nextUrl: string | null;
readonly prefetchKind?: PrefetchKind;
readonly isHmrRefresh?: boolean;
}
export type FetchServerResponseResult = {
flightData: NormalizedFlightData[] | string;
canonicalUrl: URL | undefined;
couldBeIntercepted: boolean;
prerendered: boolean;
postponed: boolean;
staleTime: number;
};
export type RequestHeaders = {
[RSC_HEADER]?: '1';
[NEXT_ROUTER_STATE_TREE_HEADER]?: string;
[NEXT_URL]?: string;
[NEXT_ROUTER_PREFETCH_HEADER]?: '1';
[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]?: string;
'x-deployment-id'?: string;
[NEXT_HMR_REFRESH_HEADER]?: '1';
'Next-Test-Fetch-Priority'?: RequestInit['priority'];
};
export declare function urlToUrlWithoutFlightMarker(url: string): URL;
/**
* Fetch the flight data for the provided url. Takes in the current router state
* to decide what to render server-side.
*/
export declare function fetchServerResponse(url: URL, options: FetchServerResponseOptions): Promise<FetchServerResponseResult>;
export declare function createFetch(url: URL, headers: RequestHeaders, fetchPriority: 'auto' | 'high' | 'low' | null, signal?: AbortSignal): Promise<Response>;
export declare function createFromNextReadableStream(flightStream: ReadableStream<Uint8Array>): Promise<unknown>;

View File

@@ -0,0 +1,249 @@
'use client';
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
createFetch: null,
createFromNextReadableStream: null,
fetchServerResponse: null,
urlToUrlWithoutFlightMarker: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
createFetch: function() {
return createFetch;
},
createFromNextReadableStream: function() {
return createFromNextReadableStream;
},
fetchServerResponse: function() {
return fetchServerResponse;
},
urlToUrlWithoutFlightMarker: function() {
return urlToUrlWithoutFlightMarker;
}
});
const _approuterheaders = require("../app-router-headers");
const _appcallserver = require("../../app-call-server");
const _appfindsourcemapurl = require("../../app-find-source-map-url");
const _routerreducertypes = require("./router-reducer-types");
const _flightdatahelpers = require("../../flight-data-helpers");
const _appbuildid = require("../../app-build-id");
const _setcachebustingsearchparam = require("./set-cache-busting-search-param");
// @ts-ignore
// eslint-disable-next-line import/no-extraneous-dependencies
// import { createFromReadableStream } from 'react-server-dom-webpack/client'
const { createFromReadableStream } = !!process.env.NEXT_RUNTIME ? require('react-server-dom-webpack/client.edge') : require('react-server-dom-webpack/client');
function urlToUrlWithoutFlightMarker(url) {
const urlWithoutFlightParameters = new URL(url, location.origin);
urlWithoutFlightParameters.searchParams.delete(_approuterheaders.NEXT_RSC_UNION_QUERY);
if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export' && urlWithoutFlightParameters.pathname.endsWith('.txt')) {
const { pathname } = urlWithoutFlightParameters;
const length = pathname.endsWith('/index.txt') ? 10 : 4;
// Slice off `/index.txt` or `.txt` from the end of the pathname
urlWithoutFlightParameters.pathname = pathname.slice(0, -length);
}
}
return urlWithoutFlightParameters;
}
function doMpaNavigation(url) {
return {
flightData: urlToUrlWithoutFlightMarker(url).toString(),
canonicalUrl: undefined,
couldBeIntercepted: false,
prerendered: false,
postponed: false,
staleTime: -1
};
}
let abortController = new AbortController();
if (typeof window !== 'undefined') {
// Abort any in-flight requests when the page is unloaded, e.g. due to
// reloading the page or performing hard navigations. This allows us to ignore
// what would otherwise be a thrown TypeError when the browser cancels the
// requests.
window.addEventListener('pagehide', ()=>{
abortController.abort();
});
// Use a fresh AbortController instance on pageshow, e.g. when navigating back
// and the JavaScript execution context is restored by the browser.
window.addEventListener('pageshow', ()=>{
abortController = new AbortController();
});
}
async function fetchServerResponse(url, options) {
const { flightRouterState, nextUrl, prefetchKind } = options;
const headers = {
// Enable flight response
[_approuterheaders.RSC_HEADER]: '1',
// Provide the current router state
[_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(JSON.stringify(flightRouterState))
};
/**
* Three cases:
* - `prefetchKind` is `undefined`, it means it's a normal navigation, so we want to prefetch the page data fully
* - `prefetchKind` is `full` - we want to prefetch the whole page so same as above
* - `prefetchKind` is `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully
*/ if (prefetchKind === _routerreducertypes.PrefetchKind.AUTO) {
headers[_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER] = '1';
}
if (process.env.NODE_ENV === 'development' && options.isHmrRefresh) {
headers[_approuterheaders.NEXT_HMR_REFRESH_HEADER] = '1';
}
if (nextUrl) {
headers[_approuterheaders.NEXT_URL] = nextUrl;
}
try {
var _res_headers_get;
// When creating a "temporary" prefetch (the "on-demand" prefetch that gets created on navigation, if one doesn't exist)
// we send the request with a "high" priority as it's in response to a user interaction that could be blocking a transition.
// Otherwise, all other prefetches are sent with a "low" priority.
// We use "auto" for in all other cases to match the existing default, as this function is shared outside of prefetching.
const fetchPriority = prefetchKind ? prefetchKind === _routerreducertypes.PrefetchKind.TEMPORARY ? 'high' : 'low' : 'auto';
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';
}
}
}
const res = await createFetch(url, headers, fetchPriority, abortController.signal);
const responseUrl = urlToUrlWithoutFlightMarker(res.url);
const canonicalUrl = res.redirected ? responseUrl : undefined;
const contentType = res.headers.get('content-type') || '';
const interception = !!((_res_headers_get = res.headers.get('vary')) == null ? void 0 : _res_headers_get.includes(_approuterheaders.NEXT_URL));
const postponed = !!res.headers.get(_approuterheaders.NEXT_DID_POSTPONE_HEADER);
const staleTimeHeader = res.headers.get(_approuterheaders.NEXT_ROUTER_STALE_TIME_HEADER);
const staleTime = staleTimeHeader !== null ? parseInt(staleTimeHeader, 10) : -1;
let isFlightResponse = contentType.startsWith(_approuterheaders.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.
if (process.env.NODE_ENV !== 'production' && !process.env.TURBOPACK) {
await require('../react-dev-overlay/app/hot-reloader-client').waitForWebpackRuntimeHotUpdate();
}
// Handle the `fetch` readable stream that can be unwrapped by `React.use`.
const flightStream = postponed ? createUnclosingPrefetchStream(res.body) : res.body;
const response = await createFromNextReadableStream(flightStream);
if ((0, _appbuildid.getAppBuildId)() !== response.b) {
return doMpaNavigation(res.url);
}
return {
flightData: (0, _flightdatahelpers.normalizeFlightData)(response.f),
canonicalUrl: canonicalUrl,
couldBeIntercepted: interception,
prerendered: response.S,
postponed,
staleTime
};
} catch (err) {
if (!abortController.signal.aborted) {
console.error("Failed to fetch RSC payload for " + url + ". 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 {
flightData: url.toString(),
canonicalUrl: undefined,
couldBeIntercepted: false,
prerendered: false,
postponed: false,
staleTime: -1
};
}
}
function createFetch(url, headers, fetchPriority, signal) {
const fetchUrl = new URL(url);
// 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.
(0, _setcachebustingsearchparam.setCacheBustingSearchParam)(fetchUrl, headers);
if (process.env.__NEXT_TEST_MODE && fetchPriority !== null) {
headers['Next-Test-Fetch-Priority'] = fetchPriority;
}
if (process.env.NEXT_DEPLOYMENT_ID) {
headers['x-deployment-id'] = process.env.NEXT_DEPLOYMENT_ID;
}
return fetch(fetchUrl, {
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
credentials: 'same-origin',
headers,
priority: fetchPriority || undefined,
signal
});
}
function createFromNextReadableStream(flightStream) {
return createFromReadableStream(flightStream, {
callServer: _appcallserver.callServer,
findSourceMapURL: _appfindsourcemapurl.findSourceMapURL
});
}
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;
}
}
});
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=fetch-server-response.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { PrefetchCacheEntry } from './router-reducer-types';
import type { NormalizedFlightData } from '../../flight-data-helpers';
/**
* Fill cache with rsc based on flightDataPath
*/
export declare function fillCacheWithNewSubTreeData(newCache: CacheNode, existingCache: CacheNode, flightData: NormalizedFlightData, prefetchEntry?: PrefetchCacheEntry): void;
export declare function fillCacheWithNewSubTreeDataButOnlyLoading(newCache: CacheNode, existingCache: CacheNode, flightData: NormalizedFlightData, prefetchEntry?: PrefetchCacheEntry): void;

View File

@@ -0,0 +1,110 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
fillCacheWithNewSubTreeData: null,
fillCacheWithNewSubTreeDataButOnlyLoading: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
fillCacheWithNewSubTreeData: function() {
return fillCacheWithNewSubTreeData;
},
fillCacheWithNewSubTreeDataButOnlyLoading: function() {
return fillCacheWithNewSubTreeDataButOnlyLoading;
}
});
const _invalidatecachebyrouterstate = require("./invalidate-cache-by-router-state");
const _filllazyitemstillleafwithhead = require("./fill-lazy-items-till-leaf-with-head");
const _createroutercachekey = require("./create-router-cache-key");
const _segment = require("../../../shared/lib/segment");
/**
* Common logic for filling cache with new sub tree data.
*/ function fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, fillLazyItems) {
const { segmentPath, seedData: cacheNodeSeedData, tree: treePatch, head } = flightData;
let newCacheNode = newCache;
let existingCacheNode = existingCache;
for(let i = 0; i < segmentPath.length; i += 2){
const parallelRouteKey = segmentPath[i];
const segment = segmentPath[i + 1];
// segmentPath is a repeating tuple of parallelRouteKey and segment
// we know we've hit the last entry we've reached our final pair
const isLastEntry = i === segmentPath.length - 2;
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segment);
const existingChildSegmentMap = existingCacheNode.parallelRoutes.get(parallelRouteKey);
if (!existingChildSegmentMap) {
continue;
}
let childSegmentMap = newCacheNode.parallelRoutes.get(parallelRouteKey);
if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
childSegmentMap = new Map(existingChildSegmentMap);
newCacheNode.parallelRoutes.set(parallelRouteKey, childSegmentMap);
}
const existingChildCacheNode = existingChildSegmentMap.get(cacheKey);
let childCacheNode = childSegmentMap.get(cacheKey);
if (isLastEntry) {
if (cacheNodeSeedData && (!childCacheNode || !childCacheNode.lazyData || childCacheNode === existingChildCacheNode)) {
const incomingSegment = cacheNodeSeedData[0];
const rsc = cacheNodeSeedData[1];
const loading = cacheNodeSeedData[3];
childCacheNode = {
lazyData: null,
// When `fillLazyItems` is false, we only want to fill the RSC data for the layout,
// not the page segment.
rsc: fillLazyItems || incomingSegment !== _segment.PAGE_SEGMENT_KEY ? rsc : null,
prefetchRsc: null,
head: null,
prefetchHead: null,
loading,
parallelRoutes: fillLazyItems && existingChildCacheNode ? new Map(existingChildCacheNode.parallelRoutes) : new Map()
};
if (existingChildCacheNode && fillLazyItems) {
(0, _invalidatecachebyrouterstate.invalidateCacheByRouterState)(childCacheNode, existingChildCacheNode, treePatch);
}
if (fillLazyItems) {
(0, _filllazyitemstillleafwithhead.fillLazyItemsTillLeafWithHead)(childCacheNode, existingChildCacheNode, treePatch, cacheNodeSeedData, head, prefetchEntry);
}
childSegmentMap.set(cacheKey, childCacheNode);
}
continue;
}
if (!childCacheNode || !existingChildCacheNode) {
continue;
}
if (childCacheNode === existingChildCacheNode) {
childCacheNode = {
lazyData: childCacheNode.lazyData,
rsc: childCacheNode.rsc,
prefetchRsc: childCacheNode.prefetchRsc,
head: childCacheNode.head,
prefetchHead: childCacheNode.prefetchHead,
parallelRoutes: new Map(childCacheNode.parallelRoutes),
loading: childCacheNode.loading
};
childSegmentMap.set(cacheKey, childCacheNode);
}
// Move deeper into the cache nodes
newCacheNode = childCacheNode;
existingCacheNode = existingChildCacheNode;
}
}
function fillCacheWithNewSubTreeData(newCache, existingCache, flightData, prefetchEntry) {
fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, true);
}
function fillCacheWithNewSubTreeDataButOnlyLoading(newCache, existingCache, flightData, prefetchEntry) {
fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, false);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=fill-cache-with-new-subtree-data.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { FlightRouterState, CacheNodeSeedData } from '../../../server/app-render/types';
import { type PrefetchCacheEntry } from './router-reducer-types';
export declare function fillLazyItemsTillLeafWithHead(newCache: CacheNode, existingCache: CacheNode | undefined, routerState: FlightRouterState, cacheNodeSeedData: CacheNodeSeedData | null, head: React.ReactNode, prefetchEntry: PrefetchCacheEntry | undefined): void;

View File

@@ -0,0 +1,144 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "fillLazyItemsTillLeafWithHead", {
enumerable: true,
get: function() {
return fillLazyItemsTillLeafWithHead;
}
});
const _createroutercachekey = require("./create-router-cache-key");
const _routerreducertypes = require("./router-reducer-types");
function fillLazyItemsTillLeafWithHead(newCache, existingCache, routerState, cacheNodeSeedData, head, prefetchEntry) {
const isLastSegment = Object.keys(routerState[1]).length === 0;
if (isLastSegment) {
newCache.head = head;
return;
}
// Remove segment that we got data for so that it is filled in during rendering of rsc.
for(const key in routerState[1]){
const parallelRouteState = routerState[1][key];
const segmentForParallelRoute = parallelRouteState[0];
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segmentForParallelRoute);
// TODO: We should traverse the cacheNodeSeedData tree instead of the router
// state tree. Ideally, they would always be the same shape, but because of
// the loading.js pattern, cacheNodeSeedData sometimes only represents a
// partial tree. That's why this node is sometimes null. Once PPR lands,
// loading.js will no longer have special behavior and we can traverse the
// data tree instead.
//
// We should also consider merging the router state tree and the data tree
// in the response format, so that we don't have to send the keys twice.
// Then the client can convert them into separate representations.
const parallelSeedData = cacheNodeSeedData !== null && cacheNodeSeedData[2][key] !== undefined ? cacheNodeSeedData[2][key] : null;
if (existingCache) {
const existingParallelRoutesCacheNode = existingCache.parallelRoutes.get(key);
if (existingParallelRoutesCacheNode) {
const hasReusablePrefetch = (prefetchEntry == null ? void 0 : prefetchEntry.kind) === 'auto' && prefetchEntry.status === _routerreducertypes.PrefetchCacheEntryStatus.reusable;
let parallelRouteCacheNode = new Map(existingParallelRoutesCacheNode);
const existingCacheNode = parallelRouteCacheNode.get(cacheKey);
let newCacheNode;
if (parallelSeedData !== null) {
// New data was sent from the server.
const seedNode = parallelSeedData[1];
const loading = parallelSeedData[3];
newCacheNode = {
lazyData: null,
rsc: seedNode,
// This is a PPR-only field. When PPR is enabled, we shouldn't hit
// this path during a navigation, but until PPR is fully implemented
// yet it's possible the existing node does have a non-null
// `prefetchRsc`. As an incremental step, we'll just de-opt to the
// old behavior — no PPR value.
prefetchRsc: null,
head: null,
prefetchHead: null,
loading,
parallelRoutes: new Map(existingCacheNode == null ? void 0 : existingCacheNode.parallelRoutes)
};
} else if (hasReusablePrefetch && existingCacheNode) {
// No new data was sent from the server, but the existing cache node
// was prefetched, so we should reuse that.
newCacheNode = {
lazyData: existingCacheNode.lazyData,
rsc: existingCacheNode.rsc,
// This is a PPR-only field. Unlike the previous branch, since we're
// just cloning the existing cache node, we might as well keep the
// PPR value, if it exists.
prefetchRsc: existingCacheNode.prefetchRsc,
head: existingCacheNode.head,
prefetchHead: existingCacheNode.prefetchHead,
parallelRoutes: new Map(existingCacheNode.parallelRoutes),
loading: existingCacheNode.loading
};
} else {
// No data available for this node. This will trigger a lazy fetch
// during render.
newCacheNode = {
lazyData: null,
rsc: null,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(existingCacheNode == null ? void 0 : existingCacheNode.parallelRoutes),
loading: null
};
}
// Overrides the cache key with the new cache node.
parallelRouteCacheNode.set(cacheKey, newCacheNode);
// Traverse deeper to apply the head / fill lazy items till the head.
fillLazyItemsTillLeafWithHead(newCacheNode, existingCacheNode, parallelRouteState, parallelSeedData ? parallelSeedData : null, head, prefetchEntry);
newCache.parallelRoutes.set(key, parallelRouteCacheNode);
continue;
}
}
let newCacheNode;
if (parallelSeedData !== null) {
// New data was sent from the server.
const seedNode = parallelSeedData[1];
const loading = parallelSeedData[3];
newCacheNode = {
lazyData: null,
rsc: seedNode,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(),
loading
};
} else {
// No data available for this node. This will trigger a lazy fetch
// during render.
newCacheNode = {
lazyData: null,
rsc: null,
prefetchRsc: null,
head: null,
prefetchHead: null,
parallelRoutes: new Map(),
loading: null
};
}
const existingParallelRoutes = newCache.parallelRoutes.get(key);
if (existingParallelRoutes) {
existingParallelRoutes.set(cacheKey, newCacheNode);
} else {
newCache.parallelRoutes.set(key, new Map([
[
cacheKey,
newCacheNode
]
]));
}
fillLazyItemsTillLeafWithHead(newCacheNode, undefined, parallelRouteState, parallelSeedData, head, prefetchEntry);
}
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=fill-lazy-items-till-leaf-with-head.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
import type { Mutable, ReadonlyReducerState, ReducerState } from './router-reducer-types';
export declare function handleMutable(state: ReadonlyReducerState, mutable: Mutable): ReducerState;

View File

@@ -0,0 +1,64 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "handleMutable", {
enumerable: true,
get: function() {
return handleMutable;
}
});
const _computechangedpath = require("./compute-changed-path");
function isNotUndefined(value) {
return typeof value !== 'undefined';
}
function handleMutable(state, mutable) {
var _mutable_shouldScroll;
// shouldScroll is true by default, can override to false.
const shouldScroll = (_mutable_shouldScroll = mutable.shouldScroll) != null ? _mutable_shouldScroll : true;
let nextUrl = state.nextUrl;
if (isNotUndefined(mutable.patchedTree)) {
// If we received a patched tree, we need to compute the changed path.
const changedPath = (0, _computechangedpath.computeChangedPath)(state.tree, mutable.patchedTree);
if (changedPath) {
// If the tree changed, we need to update the 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
}
var _mutable_scrollableSegments;
return {
// Set href.
canonicalUrl: isNotUndefined(mutable.canonicalUrl) ? mutable.canonicalUrl === state.canonicalUrl ? state.canonicalUrl : mutable.canonicalUrl : state.canonicalUrl,
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 == null ? void 0 : 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 = mutable == null ? void 0 : mutable.scrollableSegments) != null ? _mutable_scrollableSegments : state.focusAndScrollRef.segmentPaths : []
},
// Apply cache.
cache: mutable.cache ? mutable.cache : state.cache,
prefetchCache: mutable.prefetchCache ? mutable.prefetchCache : state.prefetchCache,
// Apply patched router state.
tree: isNotUndefined(mutable.patchedTree) ? mutable.patchedTree : state.tree,
nextUrl
};
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=handle-mutable.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
import type { FlightRouterState } from '../../../server/app-render/types';
import type { ReadonlyReducerState, ReducerActions } from './router-reducer-types';
/**
* Handles the case where the client router attempted to patch the tree but, due to a mismatch, the patch failed.
* This will perform an MPA navigation to return the router to a valid state.
*/
export declare function handleSegmentMismatch(state: ReadonlyReducerState, action: ReducerActions, treePatch: FlightRouterState): import("./router-reducer-types").ReducerState;

View File

@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "handleSegmentMismatch", {
enumerable: true,
get: function() {
return handleSegmentMismatch;
}
});
const _navigatereducer = require("./reducers/navigate-reducer");
function handleSegmentMismatch(state, action, treePatch) {
if (process.env.NODE_ENV === 'development') {
console.warn('Performing hard navigation because your application experienced an unrecoverable error. If this keeps occurring, please file a Next.js issue.\n\n' + 'Reason: Segment mismatch\n' + ("Last Action: " + action.type + "\n\n") + ("Current Tree: " + JSON.stringify(state.tree) + "\n\n") + ("Tree Patch Payload: " + JSON.stringify(treePatch)));
}
return (0, _navigatereducer.handleExternalUrl)(state, {}, state.canonicalUrl, true);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=handle-segment-mismatch.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/handle-segment-mismatch.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../server/app-render/types'\nimport { handleExternalUrl } from './reducers/navigate-reducer'\nimport type {\n ReadonlyReducerState,\n ReducerActions,\n} from './router-reducer-types'\n\n/**\n * Handles the case where the client router attempted to patch the tree but, due to a mismatch, the patch failed.\n * This will perform an MPA navigation to return the router to a valid state.\n */\nexport function handleSegmentMismatch(\n state: ReadonlyReducerState,\n action: ReducerActions,\n treePatch: FlightRouterState\n) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n 'Performing hard navigation because your application experienced an unrecoverable error. If this keeps occurring, please file a Next.js issue.\\n\\n' +\n 'Reason: Segment mismatch\\n' +\n `Last Action: ${action.type}\\n\\n` +\n `Current Tree: ${JSON.stringify(state.tree)}\\n\\n` +\n `Tree Patch Payload: ${JSON.stringify(treePatch)}`\n )\n }\n\n return handleExternalUrl(state, {}, state.canonicalUrl, true)\n}\n"],"names":["handleSegmentMismatch","state","action","treePatch","process","env","NODE_ENV","console","warn","type","JSON","stringify","tree","handleExternalUrl","canonicalUrl"],"mappings":";;;;+BAWgBA;;;eAAAA;;;iCAVkB;AAU3B,SAASA,sBACdC,KAA2B,EAC3BC,MAAsB,EACtBC,SAA4B;IAE5B,IAAIC,QAAQC,GAAG,CAACC,QAAQ,KAAK,eAAe;QAC1CC,QAAQC,IAAI,CACV,sJACE,+BACA,CAAA,AAAC,kBAAeN,OAAOO,IAAI,GAAC,MAAI,IAChC,CAAA,AAAC,mBAAgBC,KAAKC,SAAS,CAACV,MAAMW,IAAI,IAAE,MAAI,IAChD,CAAA,AAAC,yBAAsBF,KAAKC,SAAS,CAACR,UAAW;IAEvD;IAEA,OAAOU,IAAAA,kCAAiB,EAACZ,OAAO,CAAC,GAAGA,MAAMa,YAAY,EAAE;AAC1D"}

View File

@@ -0,0 +1,6 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { FlightSegmentPath } from '../../../server/app-render/types';
/**
* Fill cache up to the end of the flightSegmentPath, invalidating anything below it.
*/
export declare function invalidateCacheBelowFlightSegmentPath(newCache: CacheNode, existingCache: CacheNode, flightSegmentPath: FlightSegmentPath): void;

View File

@@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "invalidateCacheBelowFlightSegmentPath", {
enumerable: true,
get: function() {
return invalidateCacheBelowFlightSegmentPath;
}
});
const _createroutercachekey = require("./create-router-cache-key");
const _flightdatahelpers = require("../../flight-data-helpers");
function invalidateCacheBelowFlightSegmentPath(newCache, existingCache, flightSegmentPath) {
const isLastEntry = flightSegmentPath.length <= 2;
const [parallelRouteKey, segment] = flightSegmentPath;
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segment);
const existingChildSegmentMap = existingCache.parallelRoutes.get(parallelRouteKey);
if (!existingChildSegmentMap) {
// Bailout because the existing cache does not have the path to the leaf node
// Will trigger lazy fetch in layout-router because of missing segment
return;
}
let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey);
if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {
childSegmentMap = new Map(existingChildSegmentMap);
newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap);
}
// In case of last entry don't copy further down.
if (isLastEntry) {
childSegmentMap.delete(cacheKey);
return;
}
const existingChildCacheNode = existingChildSegmentMap.get(cacheKey);
let childCacheNode = childSegmentMap.get(cacheKey);
if (!childCacheNode || !existingChildCacheNode) {
// Bailout because the existing cache does not have the path to the leaf node
// Will trigger lazy fetch in layout-router because of missing segment
return;
}
if (childCacheNode === existingChildCacheNode) {
childCacheNode = {
lazyData: childCacheNode.lazyData,
rsc: childCacheNode.rsc,
prefetchRsc: childCacheNode.prefetchRsc,
head: childCacheNode.head,
prefetchHead: childCacheNode.prefetchHead,
parallelRoutes: new Map(childCacheNode.parallelRoutes)
};
childSegmentMap.set(cacheKey, childCacheNode);
}
invalidateCacheBelowFlightSegmentPath(childCacheNode, existingChildCacheNode, (0, _flightdatahelpers.getNextFlightSegmentPath)(flightSegmentPath));
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=invalidate-cache-below-flight-segmentpath.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.ts"],"sourcesContent":["import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'\nimport type { FlightSegmentPath } from '../../../server/app-render/types'\nimport { createRouterCacheKey } from './create-router-cache-key'\nimport { getNextFlightSegmentPath } from '../../flight-data-helpers'\n\n/**\n * Fill cache up to the end of the flightSegmentPath, invalidating anything below it.\n */\nexport function invalidateCacheBelowFlightSegmentPath(\n newCache: CacheNode,\n existingCache: CacheNode,\n flightSegmentPath: FlightSegmentPath\n): void {\n const isLastEntry = flightSegmentPath.length <= 2\n const [parallelRouteKey, segment] = flightSegmentPath\n\n const cacheKey = createRouterCacheKey(segment)\n\n const existingChildSegmentMap =\n existingCache.parallelRoutes.get(parallelRouteKey)\n\n if (!existingChildSegmentMap) {\n // Bailout because the existing cache does not have the path to the leaf node\n // Will trigger lazy fetch in layout-router because of missing segment\n return\n }\n\n let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey)\n if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) {\n childSegmentMap = new Map(existingChildSegmentMap)\n newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap)\n }\n\n // In case of last entry don't copy further down.\n if (isLastEntry) {\n childSegmentMap.delete(cacheKey)\n return\n }\n\n const existingChildCacheNode = existingChildSegmentMap.get(cacheKey)\n let childCacheNode = childSegmentMap.get(cacheKey)\n\n if (!childCacheNode || !existingChildCacheNode) {\n // Bailout because the existing cache does not have the path to the leaf node\n // Will trigger lazy fetch in layout-router because of missing segment\n return\n }\n\n if (childCacheNode === existingChildCacheNode) {\n childCacheNode = {\n lazyData: childCacheNode.lazyData,\n rsc: childCacheNode.rsc,\n prefetchRsc: childCacheNode.prefetchRsc,\n head: childCacheNode.head,\n prefetchHead: childCacheNode.prefetchHead,\n parallelRoutes: new Map(childCacheNode.parallelRoutes),\n } as CacheNode\n childSegmentMap.set(cacheKey, childCacheNode)\n }\n\n invalidateCacheBelowFlightSegmentPath(\n childCacheNode,\n existingChildCacheNode,\n getNextFlightSegmentPath(flightSegmentPath)\n )\n}\n"],"names":["invalidateCacheBelowFlightSegmentPath","newCache","existingCache","flightSegmentPath","isLastEntry","length","parallelRouteKey","segment","cacheKey","createRouterCacheKey","existingChildSegmentMap","parallelRoutes","get","childSegmentMap","Map","set","delete","existingChildCacheNode","childCacheNode","lazyData","rsc","prefetchRsc","head","prefetchHead","getNextFlightSegmentPath"],"mappings":";;;;+BAQgBA;;;eAAAA;;;sCANqB;mCACI;AAKlC,SAASA,sCACdC,QAAmB,EACnBC,aAAwB,EACxBC,iBAAoC;IAEpC,MAAMC,cAAcD,kBAAkBE,MAAM,IAAI;IAChD,MAAM,CAACC,kBAAkBC,QAAQ,GAAGJ;IAEpC,MAAMK,WAAWC,IAAAA,0CAAoB,EAACF;IAEtC,MAAMG,0BACJR,cAAcS,cAAc,CAACC,GAAG,CAACN;IAEnC,IAAI,CAACI,yBAAyB;QAC5B,6EAA6E;QAC7E,sEAAsE;QACtE;IACF;IAEA,IAAIG,kBAAkBZ,SAASU,cAAc,CAACC,GAAG,CAACN;IAClD,IAAI,CAACO,mBAAmBA,oBAAoBH,yBAAyB;QACnEG,kBAAkB,IAAIC,IAAIJ;QAC1BT,SAASU,cAAc,CAACI,GAAG,CAACT,kBAAkBO;IAChD;IAEA,iDAAiD;IACjD,IAAIT,aAAa;QACfS,gBAAgBG,MAAM,CAACR;QACvB;IACF;IAEA,MAAMS,yBAAyBP,wBAAwBE,GAAG,CAACJ;IAC3D,IAAIU,iBAAiBL,gBAAgBD,GAAG,CAACJ;IAEzC,IAAI,CAACU,kBAAkB,CAACD,wBAAwB;QAC9C,6EAA6E;QAC7E,sEAAsE;QACtE;IACF;IAEA,IAAIC,mBAAmBD,wBAAwB;QAC7CC,iBAAiB;YACfC,UAAUD,eAAeC,QAAQ;YACjCC,KAAKF,eAAeE,GAAG;YACvBC,aAAaH,eAAeG,WAAW;YACvCC,MAAMJ,eAAeI,IAAI;YACzBC,cAAcL,eAAeK,YAAY;YACzCZ,gBAAgB,IAAIG,IAAII,eAAeP,cAAc;QACvD;QACAE,gBAAgBE,GAAG,CAACP,UAAUU;IAChC;IAEAlB,sCACEkB,gBACAD,wBACAO,IAAAA,2CAAwB,EAACrB;AAE7B"}

View File

@@ -0,0 +1,6 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { FlightRouterState } from '../../../server/app-render/types';
/**
* Invalidate cache one level down from the router state.
*/
export declare function invalidateCacheByRouterState(newCache: CacheNode, existingCache: CacheNode, routerState: FlightRouterState): void;

View File

@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "invalidateCacheByRouterState", {
enumerable: true,
get: function() {
return invalidateCacheByRouterState;
}
});
const _createroutercachekey = require("./create-router-cache-key");
function invalidateCacheByRouterState(newCache, existingCache, routerState) {
// Remove segment that we got data for so that it is filled in during rendering of rsc.
for(const key in routerState[1]){
const segmentForParallelRoute = routerState[1][key][0];
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segmentForParallelRoute);
const existingParallelRoutesCacheNode = existingCache.parallelRoutes.get(key);
if (existingParallelRoutesCacheNode) {
let parallelRouteCacheNode = new Map(existingParallelRoutesCacheNode);
parallelRouteCacheNode.delete(cacheKey);
newCache.parallelRoutes.set(key, parallelRouteCacheNode);
}
}
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=invalidate-cache-by-router-state.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/invalidate-cache-by-router-state.ts"],"sourcesContent":["import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'\nimport type { FlightRouterState } from '../../../server/app-render/types'\nimport { createRouterCacheKey } from './create-router-cache-key'\n\n/**\n * Invalidate cache one level down from the router state.\n */\nexport function invalidateCacheByRouterState(\n newCache: CacheNode,\n existingCache: CacheNode,\n routerState: FlightRouterState\n): void {\n // Remove segment that we got data for so that it is filled in during rendering of rsc.\n for (const key in routerState[1]) {\n const segmentForParallelRoute = routerState[1][key][0]\n const cacheKey = createRouterCacheKey(segmentForParallelRoute)\n const existingParallelRoutesCacheNode =\n existingCache.parallelRoutes.get(key)\n if (existingParallelRoutesCacheNode) {\n let parallelRouteCacheNode = new Map(existingParallelRoutesCacheNode)\n parallelRouteCacheNode.delete(cacheKey)\n newCache.parallelRoutes.set(key, parallelRouteCacheNode)\n }\n }\n}\n"],"names":["invalidateCacheByRouterState","newCache","existingCache","routerState","key","segmentForParallelRoute","cacheKey","createRouterCacheKey","existingParallelRoutesCacheNode","parallelRoutes","get","parallelRouteCacheNode","Map","delete","set"],"mappings":";;;;+BAOgBA;;;eAAAA;;;sCALqB;AAK9B,SAASA,6BACdC,QAAmB,EACnBC,aAAwB,EACxBC,WAA8B;IAE9B,uFAAuF;IACvF,IAAK,MAAMC,OAAOD,WAAW,CAAC,EAAE,CAAE;QAChC,MAAME,0BAA0BF,WAAW,CAAC,EAAE,CAACC,IAAI,CAAC,EAAE;QACtD,MAAME,WAAWC,IAAAA,0CAAoB,EAACF;QACtC,MAAMG,kCACJN,cAAcO,cAAc,CAACC,GAAG,CAACN;QACnC,IAAII,iCAAiC;YACnC,IAAIG,yBAAyB,IAAIC,IAAIJ;YACrCG,uBAAuBE,MAAM,CAACP;YAC9BL,SAASQ,cAAc,CAACK,GAAG,CAACV,KAAKO;QACnC;IACF;AACF"}

View File

@@ -0,0 +1,2 @@
import type { FlightRouterState } from '../../../server/app-render/types';
export declare function isNavigatingToNewRootLayout(currentTree: FlightRouterState, nextTree: FlightRouterState): boolean;

View File

@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "isNavigatingToNewRootLayout", {
enumerable: true,
get: function() {
return isNavigatingToNewRootLayout;
}
});
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);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=is-navigating-to-new-root-layout.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/is-navigating-to-new-root-layout.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../server/app-render/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":";;;;+BAEgBA;;;eAAAA;;;AAAT,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"}

View File

@@ -0,0 +1,21 @@
import type { CacheNodeSeedData, FlightRouterState, FlightSegmentPath } from '../../../server/app-render/types';
import type { CacheNode, HeadData, ReadyCacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { FetchServerResponseResult } from './fetch-server-response';
type SPANavigationTask = {
route: FlightRouterState;
node: CacheNode | null;
dynamicRequestTree: FlightRouterState | null;
children: Map<string, SPANavigationTask> | null;
};
type MPANavigationTask = {
route: null;
node: null;
dynamicRequestTree: null;
children: null;
};
export type Task = SPANavigationTask | MPANavigationTask;
export declare function startPPRNavigation(oldCacheNode: CacheNode, oldRouterState: FlightRouterState, newRouterState: FlightRouterState, prefetchData: CacheNodeSeedData | null, prefetchHead: HeadData | null, isPrefetchHeadPartial: boolean, isSamePageNavigation: boolean, scrollableSegmentsResult: Array<FlightSegmentPath>): Task | null;
export declare function listenForDynamicRequest(task: SPANavigationTask, responsePromise: Promise<FetchServerResponseResult>): void;
export declare function abortTask(task: SPANavigationTask, error: any): void;
export declare function updateCacheNodeOnPopstateRestoration(oldCacheNode: CacheNode, routerState: FlightRouterState): ReadyCacheNode;
export {};

View File

@@ -0,0 +1,784 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
abortTask: null,
listenForDynamicRequest: null,
startPPRNavigation: null,
updateCacheNodeOnPopstateRestoration: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
abortTask: function() {
return abortTask;
},
listenForDynamicRequest: function() {
return listenForDynamicRequest;
},
startPPRNavigation: function() {
return startPPRNavigation;
},
updateCacheNodeOnPopstateRestoration: function() {
return updateCacheNodeOnPopstateRestoration;
}
});
const _segment = require("../../../shared/lib/segment");
const _matchsegments = require("../match-segments");
const _createroutercachekey = require("./create-router-cache-key");
const _isnavigatingtonewrootlayout = require("./is-navigating-to-new-root-layout");
const MPA_NAVIGATION_TASK = {
route: null,
node: null,
dynamicRequestTree: null,
children: null
};
function startPPRNavigation(oldCacheNode, oldRouterState, newRouterState, prefetchData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, scrollableSegmentsResult) {
const segmentPath = [];
return updateCacheNodeOnNavigation(oldCacheNode, oldRouterState, newRouterState, false, prefetchData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, segmentPath, scrollableSegmentsResult);
}
function updateCacheNodeOnNavigation(oldCacheNode, oldRouterState, newRouterState, didFindRootLayout, prefetchData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, segmentPath, scrollableSegmentsResult) {
// Diff the old and new trees to reuse the shared layouts.
const oldRouterStateChildren = oldRouterState[1];
const newRouterStateChildren = newRouterState[1];
const prefetchDataChildren = prefetchData !== null ? prefetchData[2] : null;
if (!didFindRootLayout) {
// We're currently traversing the part of the tree that was also part of
// the previous route. If we discover a root layout, then we don't need to
// trigger an MPA navigation. See beginRenderingNewRouteTree for context.
const isRootLayout = newRouterState[4] === true;
if (isRootLayout) {
// Found a matching root layout.
didFindRootLayout = true;
}
}
const oldParallelRoutes = oldCacheNode.parallelRoutes;
// Clone the current set of segment children, even if they aren't active in
// the new tree.
// TODO: We currently retain all the inactive segments indefinitely, until
// there's an explicit refresh, or a parent layout is lazily refreshed. We
// rely on this for popstate navigations, which update the Router State Tree
// but do not eagerly perform a data fetch, because they expect the segment
// data to already be in the Cache Node tree. For highly static sites that
// are mostly read-only, this may happen only rarely, causing memory to
// leak. We should figure out a better model for the lifetime of inactive
// segments, so we can maintain instant back/forward navigations without
// leaking memory indefinitely.
const prefetchParallelRoutes = new Map(oldParallelRoutes);
// As we diff the trees, we may sometimes modify (copy-on-write, not mutate)
// the Route Tree that was returned by the server — for example, in the case
// of default parallel routes, we preserve the currently active segment. To
// avoid mutating the original tree, we clone the router state children along
// the return path.
let patchedRouterStateChildren = {};
let taskChildren = null;
// Most navigations require a request to fetch additional data from the
// server, either because the data was not already prefetched, or because the
// target route contains dynamic data that cannot be prefetched.
//
// However, if the target route is fully static, and it's already completely
// loaded into the segment cache, then we can skip the server request.
//
// This starts off as `false`, and is set to `true` if any of the child
// routes requires a dynamic request.
let needsDynamicRequest = false;
// As we traverse the children, we'll construct a FlightRouterState that can
// be sent to the server to request the dynamic data. If it turns out that
// nothing in the subtree is dynamic (i.e. needsDynamicRequest is false at the
// end), then this will be discarded.
// TODO: We can probably optimize the format of this data structure to only
// include paths that are dynamic. Instead of reusing the
// FlightRouterState type.
let dynamicRequestTreeChildren = {};
for(let parallelRouteKey in newRouterStateChildren){
const newRouterStateChild = newRouterStateChildren[parallelRouteKey];
const oldRouterStateChild = oldRouterStateChildren[parallelRouteKey];
const oldSegmentMapChild = oldParallelRoutes.get(parallelRouteKey);
const prefetchDataChild = prefetchDataChildren !== null ? prefetchDataChildren[parallelRouteKey] : null;
const newSegmentChild = newRouterStateChild[0];
const newSegmentPathChild = segmentPath.concat([
parallelRouteKey,
newSegmentChild
]);
const newSegmentKeyChild = (0, _createroutercachekey.createRouterCacheKey)(newSegmentChild);
const oldSegmentChild = oldRouterStateChild !== undefined ? oldRouterStateChild[0] : undefined;
const oldCacheNodeChild = oldSegmentMapChild !== undefined ? oldSegmentMapChild.get(newSegmentKeyChild) : undefined;
let taskChild;
if (newSegmentChild === _segment.DEFAULT_SEGMENT_KEY) {
// This is another kind of leaf segment — a default route.
//
// Default routes have special behavior. When there's no matching segment
// for a parallel route, Next.js preserves the currently active segment
// during a client navigation — but not for initial render. The server
// leaves it to the client to account for this. So we need to handle
// it here.
if (oldRouterStateChild !== undefined) {
// Reuse the existing Router State for this segment. We spawn a "task"
// just to keep track of the updated router state; unlike most, it's
// already fulfilled and won't be affected by the dynamic response.
taskChild = spawnReusedTask(oldRouterStateChild);
} else {
// There's no currently active segment. Switch to the "create" path.
taskChild = beginRenderingNewRouteTree(oldRouterStateChild, newRouterStateChild, didFindRootLayout, prefetchDataChild !== undefined ? prefetchDataChild : null, prefetchHead, isPrefetchHeadPartial, newSegmentPathChild, scrollableSegmentsResult);
}
} else if (isSamePageNavigation && // Check if this is a page segment.
// TODO: We're not consistent about how we do this check. Some places
// check if the segment starts with PAGE_SEGMENT_KEY, but most seem to
// check if there any any children, which is why I'm doing it here. We
// should probably encode an empty children set as `null` though. Either
// way, we should update all the checks to be consistent.
Object.keys(newRouterStateChild[1]).length === 0) {
// We special case navigations to the exact same URL as the current
// location. It's a common UI pattern for apps to refresh when you click a
// link to the current page. So when this happens, we refresh the dynamic
// data in the page segments.
//
// Note that this does not apply if the any part of the hash or search
// query has changed. This might feel a bit weird but it makes more sense
// when you consider that the way to trigger this behavior is to click
// the same link multiple times.
//
// TODO: We should probably refresh the *entire* route when this case
// occurs, not just the page segments. Essentially treating it the same as
// a refresh() triggered by an action, which is the more explicit way of
// modeling the UI pattern described above.
//
// Also note that this only refreshes the dynamic data, not static/
// cached data. If the page segment is fully static and prefetched, the
// request is skipped. (This is also how refresh() works.)
taskChild = beginRenderingNewRouteTree(oldRouterStateChild, newRouterStateChild, didFindRootLayout, prefetchDataChild !== undefined ? prefetchDataChild : null, prefetchHead, isPrefetchHeadPartial, newSegmentPathChild, scrollableSegmentsResult);
} else if (oldRouterStateChild !== undefined && oldSegmentChild !== undefined && (0, _matchsegments.matchSegment)(newSegmentChild, oldSegmentChild)) {
if (oldCacheNodeChild !== undefined && oldRouterStateChild !== undefined) {
// This segment exists in both the old and new trees. Recursively update
// the children.
taskChild = updateCacheNodeOnNavigation(oldCacheNodeChild, oldRouterStateChild, newRouterStateChild, didFindRootLayout, prefetchDataChild, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, newSegmentPathChild, scrollableSegmentsResult);
} else {
// There's no existing Cache Node for this segment. Switch to the
// "create" path.
taskChild = beginRenderingNewRouteTree(oldRouterStateChild, newRouterStateChild, didFindRootLayout, prefetchDataChild !== undefined ? prefetchDataChild : null, prefetchHead, isPrefetchHeadPartial, newSegmentPathChild, scrollableSegmentsResult);
}
} else {
// This is a new tree. Switch to the "create" path.
taskChild = beginRenderingNewRouteTree(oldRouterStateChild, newRouterStateChild, didFindRootLayout, prefetchDataChild !== undefined ? prefetchDataChild : null, prefetchHead, isPrefetchHeadPartial, newSegmentPathChild, scrollableSegmentsResult);
}
if (taskChild !== null) {
// Recursively propagate up the child tasks.
if (taskChild.route === null) {
// One of the child tasks discovered a change to the root layout.
// Immediately unwind from this recursive traversal.
return MPA_NAVIGATION_TASK;
}
if (taskChildren === null) {
taskChildren = new Map();
}
taskChildren.set(parallelRouteKey, taskChild);
const newCacheNodeChild = taskChild.node;
if (newCacheNodeChild !== null) {
const newSegmentMapChild = new Map(oldSegmentMapChild);
newSegmentMapChild.set(newSegmentKeyChild, newCacheNodeChild);
prefetchParallelRoutes.set(parallelRouteKey, newSegmentMapChild);
}
// The child tree's route state may be different from the prefetched
// route sent by the server. We need to clone it as we traverse back up
// the tree.
const taskChildRoute = taskChild.route;
patchedRouterStateChildren[parallelRouteKey] = taskChildRoute;
const dynamicRequestTreeChild = taskChild.dynamicRequestTree;
if (dynamicRequestTreeChild !== null) {
// Something in the child tree is dynamic.
needsDynamicRequest = true;
dynamicRequestTreeChildren[parallelRouteKey] = dynamicRequestTreeChild;
} else {
dynamicRequestTreeChildren[parallelRouteKey] = taskChildRoute;
}
} else {
// The child didn't change. We can use the prefetched router state.
patchedRouterStateChildren[parallelRouteKey] = newRouterStateChild;
dynamicRequestTreeChildren[parallelRouteKey] = newRouterStateChild;
}
}
if (taskChildren === null) {
// No new tasks were spawned.
return null;
}
const newCacheNode = {
lazyData: null,
rsc: oldCacheNode.rsc,
// We intentionally aren't updating the prefetchRsc field, since this node
// is already part of the current tree, because it would be weird for
// prefetch data to be newer than the final data. It probably won't ever be
// observable anyway, but it could happen if the segment is unmounted then
// mounted again, because LayoutRouter will momentarily switch to rendering
// prefetchRsc, via useDeferredValue.
prefetchRsc: oldCacheNode.prefetchRsc,
head: oldCacheNode.head,
prefetchHead: oldCacheNode.prefetchHead,
loading: oldCacheNode.loading,
// Everything is cloned except for the children, which we computed above.
parallelRoutes: prefetchParallelRoutes
};
return {
// Return a cloned copy of the router state with updated children.
route: patchRouterStateWithNewChildren(newRouterState, patchedRouterStateChildren),
node: newCacheNode,
dynamicRequestTree: needsDynamicRequest ? patchRouterStateWithNewChildren(newRouterState, dynamicRequestTreeChildren) : null,
children: taskChildren
};
}
function beginRenderingNewRouteTree(oldRouterState, newRouterState, didFindRootLayout, prefetchData, possiblyPartialPrefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult) {
if (!didFindRootLayout) {
// The route tree changed before we reached a layout. (The highest-level
// layout in a route tree is referred to as the "root" layout.) This could
// mean that we're navigating between two different root layouts. When this
// happens, we perform a full-page (MPA-style) navigation.
//
// However, the algorithm for deciding where to start rendering a route
// (i.e. the one performed in order to reach this function) is stricter
// than the one used to detect a change in the root layout. So just because
// we're re-rendering a segment outside of the root layout does not mean we
// should trigger a full-page navigation.
//
// Specifically, we handle dynamic parameters differently: two segments are
// considered the same even if their parameter values are different.
//
// Refer to isNavigatingToNewRootLayout for details.
//
// Note that we only have to perform this extra traversal if we didn't
// already discover a root layout in the part of the tree that is unchanged.
// In the common case, this branch is skipped completely.
if (oldRouterState === undefined || (0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(oldRouterState, newRouterState)) {
// The root layout changed. Perform a full-page navigation.
return MPA_NAVIGATION_TASK;
}
}
return createCacheNodeOnNavigation(newRouterState, prefetchData, possiblyPartialPrefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult);
}
function createCacheNodeOnNavigation(routerState, prefetchData, possiblyPartialPrefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult) {
// Same traversal as updateCacheNodeNavigation, but we switch to this path
// once we reach the part of the tree that was not in the previous route. We
// don't need to diff against the old tree, we just need to create a new one.
if (prefetchData === null) {
// There's no prefetch for this segment. Everything from this point will be
// requested from the server, even if there are static children below it.
// Create a terminal task node that will later be fulfilled by
// server response.
return spawnPendingTask(routerState, null, possiblyPartialPrefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult);
}
const routerStateChildren = routerState[1];
const isPrefetchRscPartial = prefetchData[4];
// The head is assigned to every leaf segment delivered by the server. Based
// on corresponding logic in fill-lazy-items-till-leaf-with-head.ts
const isLeafSegment = Object.keys(routerStateChildren).length === 0;
// If prefetch data is available for a segment, and it's fully static (i.e.
// does not contain any dynamic holes), we don't need to request it from
// the server.
if (// Check if the segment data is partial
isPrefetchRscPartial || // Check if the head is partial (only relevant if this is a leaf segment)
isPrefetchHeadPartial && isLeafSegment) {
// We only have partial data from this segment. Like missing segments, we
// must request the full data from the server.
return spawnPendingTask(routerState, prefetchData, possiblyPartialPrefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult);
}
// The prefetched segment is fully static, so we don't need to request a new
// one from the server. Keep traversing down the tree until we reach something
// that requires a dynamic request.
const prefetchDataChildren = prefetchData[2];
const taskChildren = new Map();
const cacheNodeChildren = new Map();
let dynamicRequestTreeChildren = {};
let needsDynamicRequest = false;
if (isLeafSegment) {
// The segment path of every leaf segment (i.e. page) is collected into
// a result array. This is used by the LayoutRouter to scroll to ensure that
// new pages are visible after a navigation.
// TODO: We should use a string to represent the segment path instead of
// an array. We already use a string representation for the path when
// accessing the Segment Cache, so we can use the same one.
scrollableSegmentsResult.push(segmentPath);
} else {
for(let parallelRouteKey in routerStateChildren){
const routerStateChild = routerStateChildren[parallelRouteKey];
const prefetchDataChild = prefetchDataChildren !== null ? prefetchDataChildren[parallelRouteKey] : null;
const segmentChild = routerStateChild[0];
const segmentPathChild = segmentPath.concat([
parallelRouteKey,
segmentChild
]);
const segmentKeyChild = (0, _createroutercachekey.createRouterCacheKey)(segmentChild);
const taskChild = createCacheNodeOnNavigation(routerStateChild, prefetchDataChild, possiblyPartialPrefetchHead, isPrefetchHeadPartial, segmentPathChild, scrollableSegmentsResult);
taskChildren.set(parallelRouteKey, taskChild);
const dynamicRequestTreeChild = taskChild.dynamicRequestTree;
if (dynamicRequestTreeChild !== null) {
// Something in the child tree is dynamic.
needsDynamicRequest = true;
dynamicRequestTreeChildren[parallelRouteKey] = dynamicRequestTreeChild;
} else {
dynamicRequestTreeChildren[parallelRouteKey] = routerStateChild;
}
const newCacheNodeChild = taskChild.node;
if (newCacheNodeChild !== null) {
const newSegmentMapChild = new Map();
newSegmentMapChild.set(segmentKeyChild, newCacheNodeChild);
cacheNodeChildren.set(parallelRouteKey, newSegmentMapChild);
}
}
}
const rsc = prefetchData[1];
const loading = prefetchData[3];
return {
// Since we're inside a new route tree, unlike the
// `updateCacheNodeOnNavigation` path, the router state on the children
// tasks is always the same as the router state we pass in. So we don't need
// to clone/modify it.
route: routerState,
node: {
lazyData: null,
// Since this is a fully static segment, we don't need to use the
// `prefetchRsc` field.
rsc,
prefetchRsc: null,
head: isLeafSegment ? possiblyPartialPrefetchHead : null,
prefetchHead: null,
loading,
parallelRoutes: cacheNodeChildren
},
dynamicRequestTree: needsDynamicRequest ? patchRouterStateWithNewChildren(routerState, dynamicRequestTreeChildren) : null,
children: taskChildren
};
}
function patchRouterStateWithNewChildren(baseRouterState, newChildren) {
const clone = [
baseRouterState[0],
newChildren
];
// Based on equivalent logic in apply-router-state-patch-to-tree, but should
// confirm whether we need to copy all of these fields. Not sure the server
// ever sends, e.g. the refetch marker.
if (2 in baseRouterState) {
clone[2] = baseRouterState[2];
}
if (3 in baseRouterState) {
clone[3] = baseRouterState[3];
}
if (4 in baseRouterState) {
clone[4] = baseRouterState[4];
}
return clone;
}
function spawnPendingTask(routerState, prefetchData, prefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult) {
// Create a task that will later be fulfilled by data from the server.
// Clone the prefetched route tree and the `refetch` marker to it. We'll send
// this to the server so it knows where to start rendering.
const dynamicRequestTree = patchRouterStateWithNewChildren(routerState, routerState[1]);
dynamicRequestTree[3] = 'refetch';
const newTask = {
route: routerState,
// Corresponds to the part of the route that will be rendered on the server.
node: createPendingCacheNode(routerState, prefetchData, prefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult),
// Because this is non-null, and it gets propagated up through the parent
// tasks, the root task will know that it needs to perform a server request.
dynamicRequestTree,
children: null
};
return newTask;
}
function spawnReusedTask(reusedRouterState) {
// Create a task that reuses an existing segment, e.g. when reusing
// the current active segment in place of a default route.
return {
route: reusedRouterState,
node: null,
dynamicRequestTree: null,
children: null
};
}
function listenForDynamicRequest(task, responsePromise) {
responsePromise.then((param)=>{
let { flightData } = param;
if (typeof flightData === 'string') {
// Happens when navigating to page in `pages` from `app`. We shouldn't
// get here because should have already handled this during
// the prefetch.
return;
}
for (const normalizedFlightData of flightData){
const { segmentPath, tree: serverRouterState, seedData: dynamicData, head: dynamicHead } = normalizedFlightData;
if (!dynamicData) {
continue;
}
writeDynamicDataIntoPendingTask(task, segmentPath, serverRouterState, dynamicData, dynamicHead);
}
// Now that we've exhausted all the data we received from the server, if
// there are any remaining pending tasks in the tree, abort them now.
// If there's any missing data, it will trigger a lazy fetch.
abortTask(task, null);
}, (error)=>{
// This will trigger an error during render
abortTask(task, error);
});
}
function writeDynamicDataIntoPendingTask(rootTask, segmentPath, serverRouterState, dynamicData, dynamicHead) {
// The data sent by the server represents only a subtree of the app. We need
// to find the part of the task tree that matches the server response, and
// fulfill it using the dynamic data.
//
// segmentPath represents the parent path of subtree. It's a repeating pattern
// of parallel route key and segment:
//
// [string, Segment, string, Segment, string, Segment, ...]
//
// Iterate through the path and finish any tasks that match this payload.
let task = rootTask;
for(let i = 0; i < segmentPath.length; i += 2){
const parallelRouteKey = segmentPath[i];
const segment = segmentPath[i + 1];
const taskChildren = task.children;
if (taskChildren !== null) {
const taskChild = taskChildren.get(parallelRouteKey);
if (taskChild !== undefined) {
const taskSegment = taskChild.route[0];
if ((0, _matchsegments.matchSegment)(segment, taskSegment)) {
// Found a match for this task. Keep traversing down the task tree.
task = taskChild;
continue;
}
}
}
// We didn't find a child task that matches the server data. Exit. We won't
// abort the task, though, because a different FlightDataPath may be able to
// fulfill it (see loop in listenForDynamicRequest). We only abort tasks
// once we've run out of data.
return;
}
finishTaskUsingDynamicDataPayload(task, serverRouterState, dynamicData, dynamicHead);
}
function finishTaskUsingDynamicDataPayload(task, serverRouterState, dynamicData, dynamicHead) {
if (task.dynamicRequestTree === null) {
// Everything in this subtree is already complete. Bail out.
return;
}
// dynamicData may represent a larger subtree than the task. Before we can
// finish the task, we need to line them up.
const taskChildren = task.children;
const taskNode = task.node;
if (taskChildren === null) {
// We've reached the leaf node of the pending task. The server data tree
// lines up the pending Cache Node tree. We can now switch to the
// normal algorithm.
if (taskNode !== null) {
finishPendingCacheNode(taskNode, task.route, serverRouterState, dynamicData, dynamicHead);
// Set this to null to indicate that this task is now complete.
task.dynamicRequestTree = null;
}
return;
}
// The server returned more data than we need to finish the task. Skip over
// the extra segments until we reach the leaf task node.
const serverChildren = serverRouterState[1];
const dynamicDataChildren = dynamicData[2];
for(const parallelRouteKey in serverRouterState){
const serverRouterStateChild = serverChildren[parallelRouteKey];
const dynamicDataChild = dynamicDataChildren[parallelRouteKey];
const taskChild = taskChildren.get(parallelRouteKey);
if (taskChild !== undefined) {
const taskSegment = taskChild.route[0];
if ((0, _matchsegments.matchSegment)(serverRouterStateChild[0], taskSegment) && dynamicDataChild !== null && dynamicDataChild !== undefined) {
// Found a match for this task. Keep traversing down the task tree.
return finishTaskUsingDynamicDataPayload(taskChild, serverRouterStateChild, dynamicDataChild, dynamicHead);
}
}
// We didn't find a child task that matches the server data. We won't abort
// the task, though, because a different FlightDataPath may be able to
// fulfill it (see loop in listenForDynamicRequest). We only abort tasks
// once we've run out of data.
}
}
function createPendingCacheNode(routerState, prefetchData, prefetchHead, isPrefetchHeadPartial, segmentPath, scrollableSegmentsResult) {
const routerStateChildren = routerState[1];
const prefetchDataChildren = prefetchData !== null ? prefetchData[2] : null;
const parallelRoutes = new Map();
for(let parallelRouteKey in routerStateChildren){
const routerStateChild = routerStateChildren[parallelRouteKey];
const prefetchDataChild = prefetchDataChildren !== null ? prefetchDataChildren[parallelRouteKey] : null;
const segmentChild = routerStateChild[0];
const segmentPathChild = segmentPath.concat([
parallelRouteKey,
segmentChild
]);
const segmentKeyChild = (0, _createroutercachekey.createRouterCacheKey)(segmentChild);
const newCacheNodeChild = createPendingCacheNode(routerStateChild, prefetchDataChild === undefined ? null : prefetchDataChild, prefetchHead, isPrefetchHeadPartial, segmentPathChild, scrollableSegmentsResult);
const newSegmentMapChild = new Map();
newSegmentMapChild.set(segmentKeyChild, newCacheNodeChild);
parallelRoutes.set(parallelRouteKey, newSegmentMapChild);
}
// The head is assigned to every leaf segment delivered by the server. Based
// on corresponding logic in fill-lazy-items-till-leaf-with-head.ts
const isLeafSegment = parallelRoutes.size === 0;
if (isLeafSegment) {
// The segment path of every leaf segment (i.e. page) is collected into
// a result array. This is used by the LayoutRouter to scroll to ensure that
// new pages are visible after a navigation.
// TODO: We should use a string to represent the segment path instead of
// an array. We already use a string representation for the path when
// accessing the Segment Cache, so we can use the same one.
scrollableSegmentsResult.push(segmentPath);
}
const maybePrefetchRsc = prefetchData !== null ? prefetchData[1] : null;
const maybePrefetchLoading = prefetchData !== null ? prefetchData[3] : null;
return {
lazyData: null,
parallelRoutes: parallelRoutes,
prefetchRsc: maybePrefetchRsc !== undefined ? maybePrefetchRsc : null,
prefetchHead: isLeafSegment ? prefetchHead : [
null,
null
],
// TODO: Technically, a loading boundary could contain dynamic data. We must
// have separate `loading` and `prefetchLoading` fields to handle this, like
// we do for the segment data and head.
loading: maybePrefetchLoading !== undefined ? maybePrefetchLoading : null,
// Create a deferred promise. This will be fulfilled once the dynamic
// response is received from the server.
rsc: createDeferredRsc(),
head: isLeafSegment ? createDeferredRsc() : null
};
}
function finishPendingCacheNode(cacheNode, taskState, serverState, dynamicData, dynamicHead) {
// Writes a dynamic response into an existing Cache Node tree. This does _not_
// create a new tree, it updates the existing tree in-place. So it must follow
// the Suspense rules of cache safety — it can resolve pending promises, but
// it cannot overwrite existing data. It can add segments to the tree (because
// a missing segment will cause the layout router to suspend).
// but it cannot delete them.
//
// We must resolve every promise in the tree, or else it will suspend
// indefinitely. If we did not receive data for a segment, we will resolve its
// data promise to `null` to trigger a lazy fetch during render.
const taskStateChildren = taskState[1];
const serverStateChildren = serverState[1];
const dataChildren = dynamicData[2];
// The router state that we traverse the tree with (taskState) is the same one
// that we used to construct the pending Cache Node tree. That way we're sure
// to resolve all the pending promises.
const parallelRoutes = cacheNode.parallelRoutes;
for(let parallelRouteKey in taskStateChildren){
const taskStateChild = taskStateChildren[parallelRouteKey];
const serverStateChild = serverStateChildren[parallelRouteKey];
const dataChild = dataChildren[parallelRouteKey];
const segmentMapChild = parallelRoutes.get(parallelRouteKey);
const taskSegmentChild = taskStateChild[0];
const taskSegmentKeyChild = (0, _createroutercachekey.createRouterCacheKey)(taskSegmentChild);
const cacheNodeChild = segmentMapChild !== undefined ? segmentMapChild.get(taskSegmentKeyChild) : undefined;
if (cacheNodeChild !== undefined) {
if (serverStateChild !== undefined && (0, _matchsegments.matchSegment)(taskSegmentChild, serverStateChild[0])) {
if (dataChild !== undefined && dataChild !== null) {
// This is the happy path. Recursively update all the children.
finishPendingCacheNode(cacheNodeChild, taskStateChild, serverStateChild, dataChild, dynamicHead);
} else {
// The server never returned data for this segment. Trigger a lazy
// fetch during render. This shouldn't happen because the Route Tree
// and the Seed Data tree sent by the server should always be the same
// shape when part of the same server response.
abortPendingCacheNode(taskStateChild, cacheNodeChild, null);
}
} else {
// The server never returned data for this segment. Trigger a lazy
// fetch during render.
abortPendingCacheNode(taskStateChild, cacheNodeChild, null);
}
} else {
// The server response matches what was expected to receive, but there's
// no matching Cache Node in the task tree. This is a bug in the
// implementation because we should have created a node for every
// segment in the tree that's associated with this task.
}
}
// Use the dynamic data from the server to fulfill the deferred RSC promise
// on the Cache Node.
const rsc = cacheNode.rsc;
const dynamicSegmentData = dynamicData[1];
if (rsc === null) {
// This is a lazy cache node. We can overwrite it. This is only safe
// because we know that the LayoutRouter suspends if `rsc` is `null`.
cacheNode.rsc = dynamicSegmentData;
} else if (isDeferredRsc(rsc)) {
// This is a deferred RSC promise. We can fulfill it with the data we just
// received from the server. If it was already resolved by a different
// navigation, then this does nothing because we can't overwrite data.
rsc.resolve(dynamicSegmentData);
} else {
// This is not a deferred RSC promise, nor is it empty, so it must have
// been populated by a different navigation. We must not overwrite it.
}
// Check if this is a leaf segment. If so, it will have a `head` property with
// a pending promise that needs to be resolved with the dynamic head from
// the server.
const head = cacheNode.head;
if (isDeferredRsc(head)) {
head.resolve(dynamicHead);
}
}
function abortTask(task, error) {
const cacheNode = task.node;
if (cacheNode === null) {
// This indicates the task is already complete.
return;
}
const taskChildren = task.children;
if (taskChildren === null) {
// Reached the leaf task node. This is the root of a pending cache
// node tree.
abortPendingCacheNode(task.route, cacheNode, error);
} else {
// This is an intermediate task node. Keep traversing until we reach a
// task node with no children. That will be the root of the cache node tree
// that needs to be resolved.
for (const taskChild of taskChildren.values()){
abortTask(taskChild, error);
}
}
// Set this to null to indicate that this task is now complete.
task.dynamicRequestTree = null;
}
function abortPendingCacheNode(routerState, cacheNode, error) {
// For every pending segment in the tree, resolve its `rsc` promise to `null`
// to trigger a lazy fetch during render.
//
// Or, if an error object is provided, it will error instead.
const routerStateChildren = routerState[1];
const parallelRoutes = cacheNode.parallelRoutes;
for(let parallelRouteKey in routerStateChildren){
const routerStateChild = routerStateChildren[parallelRouteKey];
const segmentMapChild = parallelRoutes.get(parallelRouteKey);
if (segmentMapChild === undefined) {
continue;
}
const segmentChild = routerStateChild[0];
const segmentKeyChild = (0, _createroutercachekey.createRouterCacheKey)(segmentChild);
const cacheNodeChild = segmentMapChild.get(segmentKeyChild);
if (cacheNodeChild !== undefined) {
abortPendingCacheNode(routerStateChild, cacheNodeChild, error);
} else {
// This shouldn't happen because we're traversing the same tree that was
// used to construct the cache nodes in the first place.
}
}
const rsc = cacheNode.rsc;
if (isDeferredRsc(rsc)) {
if (error === null) {
// This will trigger a lazy fetch during render.
rsc.resolve(null);
} else {
// This will trigger an error during rendering.
rsc.reject(error);
}
}
// Check if this is a leaf segment. If so, it will have a `head` property with
// a pending promise that needs to be resolved. If an error was provided, we
// will not resolve it with an error, since this is rendered at the root of
// the app. We want the segment to error, not the entire app.
const head = cacheNode.head;
if (isDeferredRsc(head)) {
head.resolve(null);
}
}
function updateCacheNodeOnPopstateRestoration(oldCacheNode, routerState) {
// A popstate navigation reads data from the local cache. It does not issue
// new network requests (unless the cache entries have been evicted). So, we
// update the cache to drop the prefetch data for any segment whose dynamic
// data was already received. This prevents an unnecessary flash back to PPR
// state during a back/forward navigation.
//
// This function clones the entire cache node tree and sets the `prefetchRsc`
// field to `null` to prevent it from being rendered. We can't mutate the node
// in place because this is a concurrent data structure.
const routerStateChildren = routerState[1];
const oldParallelRoutes = oldCacheNode.parallelRoutes;
const newParallelRoutes = new Map(oldParallelRoutes);
for(let parallelRouteKey in routerStateChildren){
const routerStateChild = routerStateChildren[parallelRouteKey];
const segmentChild = routerStateChild[0];
const segmentKeyChild = (0, _createroutercachekey.createRouterCacheKey)(segmentChild);
const oldSegmentMapChild = oldParallelRoutes.get(parallelRouteKey);
if (oldSegmentMapChild !== undefined) {
const oldCacheNodeChild = oldSegmentMapChild.get(segmentKeyChild);
if (oldCacheNodeChild !== undefined) {
const newCacheNodeChild = updateCacheNodeOnPopstateRestoration(oldCacheNodeChild, routerStateChild);
const newSegmentMapChild = new Map(oldSegmentMapChild);
newSegmentMapChild.set(segmentKeyChild, newCacheNodeChild);
newParallelRoutes.set(parallelRouteKey, newSegmentMapChild);
}
}
}
// Only show prefetched data if the dynamic data is still pending.
//
// Tehnically, what we're actually checking is whether the dynamic network
// response was received. But since it's a streaming response, this does not
// mean that all the dynamic data has fully streamed in. It just means that
// _some_ of the dynamic data was received. But as a heuristic, we assume that
// the rest dynamic data will stream in quickly, so it's still better to skip
// the prefetch state.
const rsc = oldCacheNode.rsc;
const shouldUsePrefetch = isDeferredRsc(rsc) && rsc.status === 'pending';
return {
lazyData: null,
rsc,
head: oldCacheNode.head,
prefetchHead: shouldUsePrefetch ? oldCacheNode.prefetchHead : [
null,
null
],
prefetchRsc: shouldUsePrefetch ? oldCacheNode.prefetchRsc : null,
loading: oldCacheNode.loading,
// These are the cloned children we computed above
parallelRoutes: newParallelRoutes
};
}
const DEFERRED = Symbol();
// This type exists to distinguish a DeferredRsc from a Flight promise. It's a
// compromise to avoid adding an extra field on every Cache Node, which would be
// awkward because the pre-PPR parts of codebase would need to account for it,
// too. We can remove it once type Cache Node type is more settled.
function isDeferredRsc(value) {
return value && value.tag === DEFERRED;
}
function createDeferredRsc() {
let resolve;
let reject;
const pendingRsc = new Promise((res, rej)=>{
resolve = res;
reject = rej;
});
pendingRsc.status = 'pending';
pendingRsc.resolve = (value)=>{
if (pendingRsc.status === 'pending') {
const fulfilledRsc = pendingRsc;
fulfilledRsc.status = 'fulfilled';
fulfilledRsc.value = value;
resolve(value);
}
};
pendingRsc.reject = (error)=>{
if (pendingRsc.status === 'pending') {
const rejectedRsc = pendingRsc;
rejectedRsc.status = 'rejected';
rejectedRsc.reason = error;
reject(error);
}
};
pendingRsc.tag = DEFERRED;
return pendingRsc;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=ppr-navigations.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
import { type FetchServerResponseResult } from './fetch-server-response';
import { PrefetchCacheEntryStatus, type PrefetchCacheEntry, PrefetchKind, type ReadonlyReducerState } from './router-reducer-types';
export type AliasedPrefetchCacheEntry = PrefetchCacheEntry & {
/** This is a special property that indicates a prefetch entry associated with a different URL
* was returned rather than the requested URL. This signals to the router that it should only
* apply the part that doesn't depend on searchParams (specifically the loading state).
*/
aliased?: boolean;
};
/**
* Returns a prefetch cache entry if one exists. Otherwise creates a new one and enqueues a fetch request
* to retrieve the prefetch data from the server.
*/
export declare function getOrCreatePrefetchCacheEntry({ url, nextUrl, tree, prefetchCache, kind, allowAliasing, }: Pick<ReadonlyReducerState, 'nextUrl' | 'prefetchCache' | 'tree'> & {
url: URL;
kind?: PrefetchKind;
allowAliasing: boolean;
}): AliasedPrefetchCacheEntry;
/**
* Use to seed the prefetch cache with data that has already been fetched.
*/
export declare function createSeededPrefetchCacheEntry({ nextUrl, tree, prefetchCache, url, data, kind, }: Pick<ReadonlyReducerState, 'nextUrl' | 'tree' | 'prefetchCache'> & {
url: URL;
data: FetchServerResponseResult;
kind: PrefetchKind;
}): {
treeAtTimeOfPrefetch: import("../../../server/app-render/types").FlightRouterState;
data: Promise<FetchServerResponseResult>;
kind: PrefetchKind;
prefetchTime: number;
lastUsedTime: number;
staleTime: number;
key: string;
status: PrefetchCacheEntryStatus.fresh;
url: URL;
};
export declare function prunePrefetchCache(prefetchCache: ReadonlyReducerState['prefetchCache']): void;
export declare const STATIC_STALETIME_MS: number;

View File

@@ -0,0 +1,317 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
STATIC_STALETIME_MS: null,
createSeededPrefetchCacheEntry: null,
getOrCreatePrefetchCacheEntry: null,
prunePrefetchCache: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
STATIC_STALETIME_MS: function() {
return STATIC_STALETIME_MS;
},
createSeededPrefetchCacheEntry: function() {
return createSeededPrefetchCacheEntry;
},
getOrCreatePrefetchCacheEntry: function() {
return getOrCreatePrefetchCacheEntry;
},
prunePrefetchCache: function() {
return prunePrefetchCache;
}
});
const _fetchserverresponse = require("./fetch-server-response");
const _routerreducertypes = require("./router-reducer-types");
const _prefetchreducer = require("./reducers/prefetch-reducer");
const INTERCEPTION_CACHE_KEY_MARKER = '%';
/**
* Creates a cache key for the router prefetch cache
*
* @param url - The URL being navigated to
* @param nextUrl - an internal URL, primarily used for handling rewrites. Defaults to '/'.
* @return The generated prefetch cache key.
*/ function createPrefetchCacheKeyImpl(url, includeSearchParams, prefix) {
// Initially we only use the pathname as the cache key. We don't want to include
// search params so that multiple URLs with the same search parameter can re-use
// loading states.
let pathnameFromUrl = url.pathname;
// RSC responses can differ based on search params, specifically in the case where we aren't
// returning a partial response (ie with `PrefetchKind.AUTO`).
// In the auto case, since loading.js & layout.js won't have access to search params,
// we can safely re-use that cache entry. But for full prefetches, we should not
// re-use the cache entry as the response may differ.
if (includeSearchParams) {
// if we have a full prefetch, we can include the search param in the key,
// as we'll be getting back a full response. The server might have read the search
// params when generating the full response.
pathnameFromUrl += url.search;
}
if (prefix) {
return "" + prefix + INTERCEPTION_CACHE_KEY_MARKER + pathnameFromUrl;
}
return pathnameFromUrl;
}
function createPrefetchCacheKey(url, kind, nextUrl) {
return createPrefetchCacheKeyImpl(url, kind === _routerreducertypes.PrefetchKind.FULL, nextUrl);
}
function getExistingCacheEntry(url, kind, nextUrl, prefetchCache, allowAliasing) {
if (kind === void 0) kind = _routerreducertypes.PrefetchKind.TEMPORARY;
// We first check if there's a more specific interception route prefetch entry
// This is because when we detect a prefetch that corresponds with an interception route, we prefix it with nextUrl (see `createPrefetchCacheKey`)
// to avoid conflicts with other pages that may have the same URL but render different things depending on the `Next-URL` header.
for (const maybeNextUrl of [
nextUrl,
null
]){
const cacheKeyWithParams = createPrefetchCacheKeyImpl(url, true, maybeNextUrl);
const cacheKeyWithoutParams = createPrefetchCacheKeyImpl(url, false, maybeNextUrl);
// First, we check if we have a cache entry that exactly matches the URL
const cacheKeyToUse = url.search ? cacheKeyWithParams : cacheKeyWithoutParams;
const existingEntry = prefetchCache.get(cacheKeyToUse);
if (existingEntry && allowAliasing) {
// We know we're returning an aliased entry when the pathname matches but the search params don't,
const isAliased = existingEntry.url.pathname === url.pathname && existingEntry.url.search !== url.search;
if (isAliased) {
return {
...existingEntry,
aliased: true
};
}
return existingEntry;
}
// If the request contains search params, and we're not doing a full prefetch, we can return the
// param-less entry if it exists.
// This is technically covered by the check at the bottom of this function, which iterates over cache entries,
// but lets us arrive there quicker in the param-full case.
const entryWithoutParams = prefetchCache.get(cacheKeyWithoutParams);
if (process.env.NODE_ENV !== 'development' && allowAliasing && url.search && kind !== _routerreducertypes.PrefetchKind.FULL && entryWithoutParams && // We shouldn't return the aliased entry if it was relocated to a new cache key.
// Since it's rewritten, it could respond with a completely different loading state.
!entryWithoutParams.key.includes(INTERCEPTION_CACHE_KEY_MARKER)) {
return {
...entryWithoutParams,
aliased: true
};
}
}
// If we've gotten to this point, we didn't find a specific cache entry that matched
// the request URL.
// We attempt a partial match by checking if there's a cache entry with the same pathname.
// Regardless of what we find, since it doesn't correspond with the requested URL, we'll mark it "aliased".
// This will signal to the router that it should only apply the loading state on the prefetched data.
if (process.env.NODE_ENV !== 'development' && kind !== _routerreducertypes.PrefetchKind.FULL && allowAliasing) {
for (const cacheEntry of prefetchCache.values()){
if (cacheEntry.url.pathname === url.pathname && // We shouldn't return the aliased entry if it was relocated to a new cache key.
// Since it's rewritten, it could respond with a completely different loading state.
!cacheEntry.key.includes(INTERCEPTION_CACHE_KEY_MARKER)) {
return {
...cacheEntry,
aliased: true
};
}
}
}
return undefined;
}
function getOrCreatePrefetchCacheEntry(param) {
let { url, nextUrl, tree, prefetchCache, kind, allowAliasing = true } = param;
const existingCacheEntry = getExistingCacheEntry(url, kind, nextUrl, prefetchCache, allowAliasing);
if (existingCacheEntry) {
// Grab the latest status of the cache entry and update it
existingCacheEntry.status = getPrefetchEntryCacheStatus(existingCacheEntry);
// when `kind` is provided, an explicit prefetch was requested.
// if the requested prefetch is "full" and the current cache entry wasn't, we want to re-prefetch with the new intent
const switchedToFullPrefetch = existingCacheEntry.kind !== _routerreducertypes.PrefetchKind.FULL && kind === _routerreducertypes.PrefetchKind.FULL;
if (switchedToFullPrefetch) {
// If we switched to a full prefetch, validate that the existing cache entry contained partial data.
// It's possible that the cache entry was seeded with full data but has a cache type of "auto" (ie when cache entries
// are seeded but without a prefetch intent)
existingCacheEntry.data.then((prefetchResponse)=>{
const isFullPrefetch = Array.isArray(prefetchResponse.flightData) && prefetchResponse.flightData.some((flightData)=>{
// If we started rendering from the root and we returned RSC data (seedData), we already had a full prefetch.
return flightData.isRootRender && flightData.seedData !== null;
});
if (!isFullPrefetch) {
return createLazyPrefetchEntry({
tree,
url,
nextUrl,
prefetchCache,
// If we didn't get an explicit prefetch kind, we want to set a temporary kind
// rather than assuming the same intent as the previous entry, to be consistent with how we
// lazily create prefetch entries when intent is left unspecified.
kind: kind != null ? kind : _routerreducertypes.PrefetchKind.TEMPORARY
});
}
});
}
// If the existing cache entry was marked as temporary, it means it was lazily created when attempting to get an entry,
// where we didn't have the prefetch intent. Now that we have the intent (in `kind`), we want to update the entry to the more accurate kind.
if (kind && existingCacheEntry.kind === _routerreducertypes.PrefetchKind.TEMPORARY) {
existingCacheEntry.kind = kind;
}
// We've determined that the existing entry we found is still valid, so we return it.
return existingCacheEntry;
}
// If we didn't return an entry, create a new one.
return createLazyPrefetchEntry({
tree,
url,
nextUrl,
prefetchCache,
kind: kind || _routerreducertypes.PrefetchKind.TEMPORARY
});
}
/*
* Used to take an existing cache entry and prefix it with the nextUrl, if it exists.
* This ensures that we don't have conflicting cache entries for the same URL (as is the case with route interception).
*/ function prefixExistingPrefetchCacheEntry(param) {
let { url, nextUrl, prefetchCache, existingCacheKey } = param;
const existingCacheEntry = prefetchCache.get(existingCacheKey);
if (!existingCacheEntry) {
// no-op -- there wasn't an entry to move
return;
}
const newCacheKey = createPrefetchCacheKey(url, existingCacheEntry.kind, nextUrl);
prefetchCache.set(newCacheKey, {
...existingCacheEntry,
key: newCacheKey
});
prefetchCache.delete(existingCacheKey);
return newCacheKey;
}
function createSeededPrefetchCacheEntry(param) {
let { nextUrl, tree, prefetchCache, url, data, kind } = param;
// The initial cache entry technically includes full data, but it isn't explicitly prefetched -- we just seed the
// prefetch cache so that we can skip an extra prefetch request later, since we already have the data.
// if the prefetch corresponds with an interception route, we use the nextUrl to prefix the cache key
const prefetchCacheKey = data.couldBeIntercepted ? createPrefetchCacheKey(url, kind, nextUrl) : createPrefetchCacheKey(url, kind);
const prefetchEntry = {
treeAtTimeOfPrefetch: tree,
data: Promise.resolve(data),
kind,
prefetchTime: Date.now(),
lastUsedTime: Date.now(),
staleTime: -1,
key: prefetchCacheKey,
status: _routerreducertypes.PrefetchCacheEntryStatus.fresh,
url
};
prefetchCache.set(prefetchCacheKey, prefetchEntry);
return prefetchEntry;
}
/**
* Creates a prefetch entry entry and enqueues a fetch request to retrieve the data.
*/ function createLazyPrefetchEntry(param) {
let { url, kind, tree, nextUrl, prefetchCache } = param;
const prefetchCacheKey = createPrefetchCacheKey(url, kind);
// initiates the fetch request for the prefetch and attaches a listener
// to the promise to update the prefetch cache entry when the promise resolves (if necessary)
const data = _prefetchreducer.prefetchQueue.enqueue(()=>(0, _fetchserverresponse.fetchServerResponse)(url, {
flightRouterState: tree,
nextUrl,
prefetchKind: kind
}).then((prefetchResponse)=>{
// TODO: `fetchServerResponse` should be more tighly coupled to these prefetch cache operations
// to avoid drift between this cache key prefixing logic
// (which is currently directly influenced by the server response)
let newCacheKey;
if (prefetchResponse.couldBeIntercepted) {
// Determine if we need to prefix the cache key with the nextUrl
newCacheKey = prefixExistingPrefetchCacheEntry({
url,
existingCacheKey: prefetchCacheKey,
nextUrl,
prefetchCache
});
}
// If the prefetch was a cache hit, we want to update the existing cache entry to reflect that it was a full prefetch.
// This is because we know that a static response will contain the full RSC payload, and can be updated to respect the `static`
// staleTime.
if (prefetchResponse.prerendered) {
const existingCacheEntry = prefetchCache.get(// if we prefixed the cache key due to route interception, we want to use the new key. Otherwise we use the original key
newCacheKey != null ? newCacheKey : prefetchCacheKey);
if (existingCacheEntry) {
existingCacheEntry.kind = _routerreducertypes.PrefetchKind.FULL;
if (prefetchResponse.staleTime !== -1) {
// This is the stale time that was collected by the server during
// static generation. Use this in place of the default stale time.
existingCacheEntry.staleTime = prefetchResponse.staleTime;
}
}
}
return prefetchResponse;
}));
const prefetchEntry = {
treeAtTimeOfPrefetch: tree,
data,
kind,
prefetchTime: Date.now(),
lastUsedTime: null,
staleTime: -1,
key: prefetchCacheKey,
status: _routerreducertypes.PrefetchCacheEntryStatus.fresh,
url
};
prefetchCache.set(prefetchCacheKey, prefetchEntry);
return prefetchEntry;
}
function prunePrefetchCache(prefetchCache) {
for (const [href, prefetchCacheEntry] of prefetchCache){
if (getPrefetchEntryCacheStatus(prefetchCacheEntry) === _routerreducertypes.PrefetchCacheEntryStatus.expired) {
prefetchCache.delete(href);
}
}
}
// These values are set by `define-env-plugin` (based on `nextConfig.experimental.staleTimes`)
// and default to 5 minutes (static) / 0 seconds (dynamic)
const DYNAMIC_STALETIME_MS = Number(process.env.__NEXT_CLIENT_ROUTER_DYNAMIC_STALETIME) * 1000;
const STATIC_STALETIME_MS = Number(process.env.__NEXT_CLIENT_ROUTER_STATIC_STALETIME) * 1000;
function getPrefetchEntryCacheStatus(param) {
let { kind, prefetchTime, lastUsedTime, staleTime } = param;
if (staleTime !== -1) {
// `staleTime` is the value sent by the server during static generation.
// When this is available, it takes precedence over any of the heuristics
// that follow.
//
// TODO: When PPR is enabled, the server will *always* return a stale time
// when prefetching. We should never use a prefetch entry that hasn't yet
// received data from the server. So the only two cases should be 1) we use
// the server-generated stale time 2) the unresolved entry is discarded.
return Date.now() < prefetchTime + staleTime ? _routerreducertypes.PrefetchCacheEntryStatus.fresh : _routerreducertypes.PrefetchCacheEntryStatus.stale;
}
// We will re-use the cache entry data for up to the `dynamic` staletime window.
if (Date.now() < (lastUsedTime != null ? lastUsedTime : prefetchTime) + DYNAMIC_STALETIME_MS) {
return lastUsedTime ? _routerreducertypes.PrefetchCacheEntryStatus.reusable : _routerreducertypes.PrefetchCacheEntryStatus.fresh;
}
// For "auto" prefetching, we'll re-use only the loading boundary for up to `static` staletime window.
// A stale entry will only re-use the `loading` boundary, not the full data.
// This will trigger a "lazy fetch" for the full data.
if (kind === _routerreducertypes.PrefetchKind.AUTO) {
if (Date.now() < prefetchTime + STATIC_STALETIME_MS) {
return _routerreducertypes.PrefetchCacheEntryStatus.stale;
}
}
// for "full" prefetching, we'll re-use the cache entry data for up to `static` staletime window.
if (kind === _routerreducertypes.PrefetchKind.FULL) {
if (Date.now() < prefetchTime + STATIC_STALETIME_MS) {
return _routerreducertypes.PrefetchCacheEntryStatus.reusable;
}
}
return _routerreducertypes.PrefetchCacheEntryStatus.expired;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=prefetch-cache-utils.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import type { FlightRouterState } from '../../../../server/app-render/types';
import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime';
export declare function findHeadInCache(cache: CacheNode, parallelRoutes: FlightRouterState[1]): [CacheNode, string] | null;

View File

@@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "findHeadInCache", {
enumerable: true,
get: function() {
return findHeadInCache;
}
});
const _createroutercachekey = require("../create-router-cache-key");
function findHeadInCache(cache, parallelRoutes) {
return findHeadInCacheImpl(cache, parallelRoutes, '');
}
function findHeadInCacheImpl(cache, parallelRoutes, keyPrefix) {
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
];
}
// First try the 'children' parallel route if it exists
// when starting from the "root", this corresponds with the main page component
if (parallelRoutes.children) {
const [segment, childParallelRoutes] = parallelRoutes.children;
const childSegmentMap = cache.parallelRoutes.get('children');
if (childSegmentMap) {
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segment);
const cacheNode = childSegmentMap.get(cacheKey);
if (cacheNode) {
const item = findHeadInCacheImpl(cacheNode, childParallelRoutes, keyPrefix + '/' + cacheKey);
if (item) return item;
}
}
}
// if we didn't find metadata in the page slot, check the other parallel routes
for(const key in parallelRoutes){
if (key === 'children') continue; // already checked above
const [segment, childParallelRoutes] = parallelRoutes[key];
const childSegmentMap = cache.parallelRoutes.get(key);
if (!childSegmentMap) {
continue;
}
const cacheKey = (0, _createroutercachekey.createRouterCacheKey)(segment);
const cacheNode = childSegmentMap.get(cacheKey);
if (!cacheNode) {
continue;
}
const item = findHeadInCacheImpl(cacheNode, childParallelRoutes, keyPrefix + '/' + cacheKey);
if (item) {
return item;
}
}
return null;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=find-head-in-cache.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/client/components/router-reducer/reducers/find-head-in-cache.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../../server/app-render/types'\nimport type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime'\nimport { createRouterCacheKey } from '../create-router-cache-key'\n\nexport function findHeadInCache(\n cache: CacheNode,\n parallelRoutes: FlightRouterState[1]\n): [CacheNode, string] | null {\n return findHeadInCacheImpl(cache, parallelRoutes, '')\n}\n\nfunction findHeadInCacheImpl(\n cache: CacheNode,\n parallelRoutes: FlightRouterState[1],\n keyPrefix: string\n): [CacheNode, 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]\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 if (parallelRoutes.children) {\n const [segment, childParallelRoutes] = parallelRoutes.children\n const childSegmentMap = cache.parallelRoutes.get('children')\n if (childSegmentMap) {\n const cacheKey = createRouterCacheKey(segment)\n const cacheNode = childSegmentMap.get(cacheKey)\n if (cacheNode) {\n const item = findHeadInCacheImpl(\n cacheNode,\n childParallelRoutes,\n keyPrefix + '/' + cacheKey\n )\n if (item) return item\n }\n }\n }\n\n // if we didn't find metadata in the page slot, check the other parallel routes\n for (const key in parallelRoutes) {\n if (key === 'children') continue // already checked above\n\n const [segment, childParallelRoutes] = parallelRoutes[key]\n const childSegmentMap = cache.parallelRoutes.get(key)\n if (!childSegmentMap) {\n continue\n }\n\n const cacheKey = createRouterCacheKey(segment)\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 )\n if (item) {\n return item\n }\n }\n\n return null\n}\n"],"names":["findHeadInCache","cache","parallelRoutes","findHeadInCacheImpl","keyPrefix","isLastItem","Object","keys","length","children","segment","childParallelRoutes","childSegmentMap","get","cacheKey","createRouterCacheKey","cacheNode","item","key"],"mappings":";;;;+BAIgBA;;;eAAAA;;;sCAFqB;AAE9B,SAASA,gBACdC,KAAgB,EAChBC,cAAoC;IAEpC,OAAOC,oBAAoBF,OAAOC,gBAAgB;AACpD;AAEA,SAASC,oBACPF,KAAgB,EAChBC,cAAoC,EACpCE,SAAiB;IAEjB,MAAMC,aAAaC,OAAOC,IAAI,CAACL,gBAAgBM,MAAM,KAAK;IAC1D,IAAIH,YAAY;QACd,0EAA0E;QAC1E,OAAO;YAACJ;YAAOG;SAAU;IAC3B;IAEA,uDAAuD;IACvD,+EAA+E;IAC/E,IAAIF,eAAeO,QAAQ,EAAE;QAC3B,MAAM,CAACC,SAASC,oBAAoB,GAAGT,eAAeO,QAAQ;QAC9D,MAAMG,kBAAkBX,MAAMC,cAAc,CAACW,GAAG,CAAC;QACjD,IAAID,iBAAiB;YACnB,MAAME,WAAWC,IAAAA,0CAAoB,EAACL;YACtC,MAAMM,YAAYJ,gBAAgBC,GAAG,CAACC;YACtC,IAAIE,WAAW;gBACb,MAAMC,OAAOd,oBACXa,WACAL,qBACAP,YAAY,MAAMU;gBAEpB,IAAIG,MAAM,OAAOA;YACnB;QACF;IACF;IAEA,+EAA+E;IAC/E,IAAK,MAAMC,OAAOhB,eAAgB;QAChC,IAAIgB,QAAQ,YAAY,UAAS,wBAAwB;QAEzD,MAAM,CAACR,SAASC,oBAAoB,GAAGT,cAAc,CAACgB,IAAI;QAC1D,MAAMN,kBAAkBX,MAAMC,cAAc,CAACW,GAAG,CAACK;QACjD,IAAI,CAACN,iBAAiB;YACpB;QACF;QAEA,MAAME,WAAWC,IAAAA,0CAAoB,EAACL;QAEtC,MAAMM,YAAYJ,gBAAgBC,GAAG,CAACC;QACtC,IAAI,CAACE,WAAW;YACd;QACF;QAEA,MAAMC,OAAOd,oBACXa,WACAL,qBACAP,YAAY,MAAMU;QAEpB,IAAIG,MAAM;YACR,OAAOA;QACT;IACF;IAEA,OAAO;AACT"}

View File

@@ -0,0 +1,2 @@
import type { Segment } from '../../../../server/app-render/types';
export declare function getSegmentValue(segment: Segment): string;

View File

@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "getSegmentValue", {
enumerable: true,
get: function() {
return getSegmentValue;
}
});
function getSegmentValue(segment) {
return Array.isArray(segment) ? segment[1] : segment;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=get-segment-value.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/client/components/router-reducer/reducers/get-segment-value.ts"],"sourcesContent":["import type { Segment } from '../../../../server/app-render/types'\n\nexport function getSegmentValue(segment: Segment) {\n return Array.isArray(segment) ? segment[1] : segment\n}\n"],"names":["getSegmentValue","segment","Array","isArray"],"mappings":";;;;+BAEgBA;;;eAAAA;;;AAAT,SAASA,gBAAgBC,OAAgB;IAC9C,OAAOC,MAAMC,OAAO,CAACF,WAAWA,OAAO,CAAC,EAAE,GAAGA;AAC/C"}

View File

@@ -0,0 +1,2 @@
import type { FlightRouterState } from '../../../../server/app-render/types';
export declare function hasInterceptionRouteInCurrentTree([segment, parallelRoutes,]: FlightRouterState): boolean;

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "hasInterceptionRouteInCurrentTree", {
enumerable: true,
get: function() {
return hasInterceptionRouteInCurrentTree;
}
});
const _interceptionroutes = require("../../../../shared/lib/router/utils/interception-routes");
function hasInterceptionRouteInCurrentTree(param) {
let [segment, parallelRoutes] = param;
// 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')) {
return true;
}
// If segment is not an array, apply the existing string-based check
if (typeof segment === 'string' && (0, _interceptionroutes.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;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=has-interception-route-in-current-tree.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/client/components/router-reducer/reducers/has-interception-route-in-current-tree.ts"],"sourcesContent":["import type { FlightRouterState } from '../../../../server/app-render/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 (Array.isArray(segment) && (segment[2] === 'di' || segment[2] === 'ci')) {\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":["hasInterceptionRouteInCurrentTree","segment","parallelRoutes","Array","isArray","isInterceptionRouteAppPath","key"],"mappings":";;;;+BAGgBA;;;eAAAA;;;oCAF2B;AAEpC,SAASA,kCAAkC,KAG9B;IAH8B,IAAA,CAChDC,SACAC,eACkB,GAH8B;IAIhD,wGAAwG;IACxG,IAAIC,MAAMC,OAAO,CAACH,YAAaA,CAAAA,OAAO,CAAC,EAAE,KAAK,QAAQA,OAAO,CAAC,EAAE,KAAK,IAAG,GAAI;QAC1E,OAAO;IACT;IAEA,oEAAoE;IACpE,IAAI,OAAOA,YAAY,YAAYI,IAAAA,8CAA0B,EAACJ,UAAU;QACtE,OAAO;IACT;IAEA,+CAA+C;IAC/C,IAAIC,gBAAgB;QAClB,IAAK,MAAMI,OAAOJ,eAAgB;YAChC,IAAIF,kCAAkCE,cAAc,CAACI,IAAI,GAAG;gBAC1D,OAAO;YACT;QACF;IACF;IAEA,OAAO;AACT"}

View File

@@ -0,0 +1,4 @@
import type { ReadonlyReducerState, ReducerState, HmrRefreshAction } from '../router-reducer-types';
declare function hmrRefreshReducerNoop(state: ReadonlyReducerState, _action: HmrRefreshAction): ReducerState;
export declare const hmrRefreshReducer: typeof hmrRefreshReducerNoop;
export {};

View File

@@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "hmrRefreshReducer", {
enumerable: true,
get: function() {
return hmrRefreshReducer;
}
});
const _fetchserverresponse = require("../fetch-server-response");
const _createhreffromurl = require("../create-href-from-url");
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
const _navigatereducer = require("./navigate-reducer");
const _handlemutable = require("../handle-mutable");
const _applyflightdata = require("../apply-flight-data");
const _approuter = require("../../app-router");
const _handlesegmentmismatch = require("../handle-segment-mismatch");
const _hasinterceptionrouteincurrenttree = require("./has-interception-route-in-current-tree");
// A version of refresh reducer that keeps the cache around instead of wiping all of it.
function hmrRefreshReducerImpl(state, action) {
const { origin } = action;
const mutable = {};
const href = state.canonicalUrl;
mutable.preserveCustomHistoryState = false;
const cache = (0, _approuter.createEmptyCacheNode)();
// If the current tree was intercepted, the nextUrl should be included in the request.
// This is to ensure that the refresh request doesn't get intercepted, accidentally triggering the interception route.
const includeNextUrl = (0, _hasinterceptionrouteincurrenttree.hasInterceptionRouteInCurrentTree)(state.tree);
// TODO-APP: verify that `href` is not an external url.
// Fetch data from the root of the tree.
cache.lazyData = (0, _fetchserverresponse.fetchServerResponse)(new URL(href, origin), {
flightRouterState: [
state.tree[0],
state.tree[1],
state.tree[2],
'refetch'
],
nextUrl: includeNextUrl ? state.nextUrl : null,
isHmrRefresh: true
});
return cache.lazyData.then((param)=>{
let { flightData, canonicalUrl: canonicalUrlOverride } = param;
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, flightData, state.pushRef.pendingPush);
}
// Remove cache.lazyData as it has been resolved at this point.
cache.lazyData = null;
let currentTree = state.tree;
let currentCache = state.cache;
for (const normalizedFlightData of flightData){
const { tree: treePatch, isRootRender } = normalizedFlightData;
if (!isRootRender) {
// TODO-APP: handle this case better
console.log('REFRESH FAILED');
return state;
}
const newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
[
''
], currentTree, treePatch, state.canonicalUrl);
if (newTree === null) {
return (0, _handlesegmentmismatch.handleSegmentMismatch)(state, action, treePatch);
}
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, href, state.pushRef.pendingPush);
}
const canonicalUrlOverrideHref = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : undefined;
if (canonicalUrlOverride) {
mutable.canonicalUrl = canonicalUrlOverrideHref;
}
const applied = (0, _applyflightdata.applyFlightData)(currentCache, cache, normalizedFlightData);
if (applied) {
mutable.cache = cache;
currentCache = cache;
}
mutable.patchedTree = newTree;
mutable.canonicalUrl = href;
currentTree = newTree;
}
return (0, _handlemutable.handleMutable)(state, mutable);
}, ()=>state);
}
function hmrRefreshReducerNoop(state, _action) {
return state;
}
const hmrRefreshReducer = process.env.NODE_ENV === 'production' ? hmrRefreshReducerNoop : hmrRefreshReducerImpl;
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=hmr-refresh-reducer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import { type Mutable, type NavigateAction, type ReadonlyReducerState, type ReducerState } from '../router-reducer-types';
export declare function handleExternalUrl(state: ReadonlyReducerState, mutable: Mutable, url: string, pendingPush: boolean): ReducerState;
export declare function navigateReducer(state: ReadonlyReducerState, action: NavigateAction): ReducerState;

View File

@@ -0,0 +1,379 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
handleExternalUrl: null,
navigateReducer: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
handleExternalUrl: function() {
return handleExternalUrl;
},
navigateReducer: function() {
return navigateReducer;
}
});
const _fetchserverresponse = require("../fetch-server-response");
const _createhreffromurl = require("../create-href-from-url");
const _invalidatecachebelowflightsegmentpath = require("../invalidate-cache-below-flight-segmentpath");
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
const _shouldhardnavigate = require("../should-hard-navigate");
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
const _routerreducertypes = require("../router-reducer-types");
const _handlemutable = require("../handle-mutable");
const _applyflightdata = require("../apply-flight-data");
const _prefetchreducer = require("./prefetch-reducer");
const _approuter = require("../../app-router");
const _segment = require("../../../../shared/lib/segment");
const _pprnavigations = require("../ppr-navigations");
const _prefetchcacheutils = require("../prefetch-cache-utils");
const _clearcachenodedataforsegmentpath = require("../clear-cache-node-data-for-segment-path");
const _aliasedprefetchnavigations = require("../aliased-prefetch-navigations");
const _segmentcache = require("../../segment-cache");
function handleExternalUrl(state, mutable, url, pendingPush) {
mutable.mpaNavigation = true;
mutable.canonicalUrl = url;
mutable.pendingPush = pendingPush;
mutable.scrollableSegments = undefined;
return (0, _handlemutable.handleMutable)(state, mutable);
}
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;
}
function triggerLazyFetchForLeafSegments(newCache, currentCache, flightSegmentPath, treePatch) {
let appliedPatch = false;
newCache.rsc = currentCache.rsc;
newCache.prefetchRsc = currentCache.prefetchRsc;
newCache.loading = currentCache.loading;
newCache.parallelRoutes = new Map(currentCache.parallelRoutes);
const segmentPathsToFill = generateSegmentsFromPatch(treePatch).map((segment)=>[
...flightSegmentPath,
...segment
]);
for (const segmentPaths of segmentPathsToFill){
(0, _clearcachenodedataforsegmentpath.clearCacheNodeDataForSegmentPath)(newCache, currentCache, segmentPaths);
appliedPatch = true;
}
return appliedPatch;
}
function handleNavigationResult(url, state, mutable, pendingPush, result) {
switch(result.tag){
case _segmentcache.NavigationResultTag.MPA:
{
// Perform an MPA navigation.
const newUrl = result.data;
return handleExternalUrl(state, mutable, newUrl, pendingPush);
}
case _segmentcache.NavigationResultTag.NoOp:
{
// The server responded with no change to the current page. However, if
// the URL changed, we still need to update that.
const newCanonicalUrl = result.data.canonicalUrl;
mutable.canonicalUrl = newCanonicalUrl;
// 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 (0, _handlemutable.handleMutable)(state, mutable);
}
case _segmentcache.NavigationResultTag.Success:
{
// Received a new result.
mutable.cache = result.data.cacheNode;
mutable.patchedTree = result.data.flightRouterState;
mutable.canonicalUrl = result.data.canonicalUrl;
mutable.scrollableSegments = result.data.scrollableSegments;
mutable.shouldScroll = result.data.shouldScroll;
mutable.hashFragment = result.data.hash;
return (0, _handlemutable.handleMutable)(state, mutable);
}
case _segmentcache.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;
}
}
}
function navigateReducer(state, action) {
const { url, isExternalUrl, navigateType, shouldScroll, allowAliasing } = action;
const mutable = {};
const { hash } = url;
const href = (0, _createhreffromurl.createHrefFromUrl)(url);
const pendingPush = navigateType === 'push';
// we want to prune the prefetch cache on every navigation to avoid it growing too large
(0, _prefetchcacheutils.prunePrefetchCache)(state.prefetchCache);
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);
}
if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) {
// (Very Early Experimental Feature) Segment Cache
//
// Bypass the normal prefetch cache and use the new per-segment cache
// implementation instead. This is only supported if PPR is enabled, too.
//
// Temporary glue code between the router reducer and the new navigation
// implementation. Eventually we'll rewrite the router reducer to a
// state machine.
const result = (0, _segmentcache.navigate)(url, state.cache, state.tree, state.nextUrl, shouldScroll);
return handleNavigationResult(url, state, mutable, pendingPush, result);
}
const prefetchValues = (0, _prefetchcacheutils.getOrCreatePrefetchCacheEntry)({
url,
nextUrl: state.nextUrl,
tree: state.tree,
prefetchCache: state.prefetchCache,
allowAliasing
});
const { treeAtTimeOfPrefetch, data } = prefetchValues;
_prefetchreducer.prefetchQueue.bump(data);
return data.then((param)=>{
let { flightData, canonicalUrl: canonicalUrlOverride, postponed } = param;
let isFirstRead = false;
// we only want to mark this once
if (!prefetchValues.lastUsedTime) {
// important: we should only mark the cache node as dirty after we unsuspend from the call above
prefetchValues.lastUsedTime = Date.now();
isFirstRead = true;
}
if (prefetchValues.aliased) {
const result = (0, _aliasedprefetchnavigations.handleAliasedPrefetchEntry)(state, flightData, url, mutable);
// We didn't return new router state because we didn't apply the aliased entry for some reason.
// We'll re-invoke the navigation handler but ensure that we don't attempt to use the aliased entry. This
// will create an on-demand prefetch entry.
if (result === false) {
return navigateReducer(state, {
...action,
allowAliasing: false
});
}
return result;
}
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return handleExternalUrl(state, mutable, flightData, pendingPush);
}
const updatedCanonicalUrl = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : href;
const onlyHashChange = !!hash && state.canonicalUrl.split('#', 1)[0] === updatedCanonicalUrl.split('#', 1)[0];
// If only the hash has changed, the server hasn't sent us any new data. We can just update
// the mutable properties responsible for URL and scroll handling and return early.
if (onlyHashChange) {
mutable.onlyHashChange = true;
mutable.canonicalUrl = updatedCanonicalUrl;
mutable.shouldScroll = shouldScroll;
mutable.hashFragment = hash;
mutable.scrollableSegments = [];
return (0, _handlemutable.handleMutable)(state, mutable);
}
let currentTree = state.tree;
let currentCache = state.cache;
let scrollableSegments = [];
for (const normalizedFlightData of flightData){
const { pathToSegment: flightSegmentPath, seedData, head, isHeadPartial, isRootRender } = normalizedFlightData;
let treePatch = normalizedFlightData.tree;
// TODO-APP: remove ''
const flightSegmentPathWithLeadingEmpty = [
'',
...flightSegmentPath
];
// Create new tree based on the flightSegmentPath and router state patch
let newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
flightSegmentPathWithLeadingEmpty, currentTree, treePatch, href);
// If the tree patch can't be applied to the current tree then we use the tree at time of prefetch
// TODO-APP: This should instead fill in the missing pieces in `currentTree` with the data from `treeAtTimeOfPrefetch`, then apply the patch.
if (newTree === null) {
newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
flightSegmentPathWithLeadingEmpty, treeAtTimeOfPrefetch, treePatch, href);
}
if (newTree !== null) {
if (// This is just a paranoid check. When a route is PPRed, the server
// will send back a static response that's rendered from
// the root. If for some reason it doesn't, we fall back to the
// non-PPR implementation.
// TODO: We should get rid of the else branch and do all navigations
// via startPPRNavigation. The current structure is just
// an incremental step.
seedData && isRootRender && postponed) {
const task = (0, _pprnavigations.startPPRNavigation)(currentCache, currentTree, treePatch, seedData, head, isHeadPartial, false, scrollableSegments);
if (task !== null) {
if (task.route === null) {
// Detected a change to the root layout. Perform an full-
// page navigation.
return handleExternalUrl(state, mutable, href, pendingPush);
}
// Use the tree computed by startPPRNavigation instead
// of the one computed by applyRouterStatePatchToTree.
// TODO: We should remove applyRouterStatePatchToTree
// from the PPR path entirely.
const patchedRouterState = task.route;
newTree = patchedRouterState;
const newCache = task.node;
if (newCache !== null) {
// We've created a new Cache Node tree that contains a prefetched
// version of the next page. This can be rendered instantly.
mutable.cache = newCache;
}
const dynamicRequestTree = task.dynamicRequestTree;
if (dynamicRequestTree !== null) {
// The prefetched tree has dynamic holes in it. We initiate a
// dynamic request to fill them in.
//
// Do not block on the result. We'll immediately render the Cache
// Node tree and suspend on the dynamic parts. When the request
// comes in, we'll fill in missing data and ping React to
// re-render. Unlike the lazy fetching model in the non-PPR
// implementation, this is modeled as a single React update +
// streaming, rather than multiple top-level updates. (However,
// even in the new model, we'll still need to sometimes update the
// root multiple times per navigation, like if the server sends us
// a different response than we expected. For now, we revert back
// to the lazy fetching mechanism in that case.)
const dynamicRequest = (0, _fetchserverresponse.fetchServerResponse)(url, {
flightRouterState: dynamicRequestTree,
nextUrl: state.nextUrl
});
(0, _pprnavigations.listenForDynamicRequest)(task, dynamicRequest);
// We store the dynamic request on the `lazyData` property of the CacheNode
// because we're not going to await the dynamic request here. Since we're not blocking
// on the dynamic request, `layout-router` will
// task.node.lazyData = dynamicRequest
} else {
// The prefetched tree does not contain dynamic holes — it's
// fully static. We can skip the dynamic request.
}
} else {
// Nothing changed, so reuse the old cache.
// TODO: What if the head changed but not any of the segment data?
// Is that possible? If so, we should clone the whole tree and
// update the head.
newTree = treePatch;
}
} else {
// The static response does not include any dynamic holes, so
// there's no need to do a second request.
// TODO: As an incremental step this just reverts back to the
// non-PPR implementation. We can simplify this branch further,
// given that PPR prefetches are always static and return the whole
// tree. Or in the meantime we could factor it out into a
// separate function.
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
return handleExternalUrl(state, mutable, href, pendingPush);
}
const cache = (0, _approuter.createEmptyCacheNode)();
let applied = false;
if (prefetchValues.status === _routerreducertypes.PrefetchCacheEntryStatus.stale && !isFirstRead) {
// When we have a stale prefetch entry, we only want to re-use the loading state of the route we're navigating to, to support instant loading navigations
// this will trigger a lazy fetch for the actual page data by nulling the `rsc` and `prefetchRsc` values for page data,
// while copying over the `loading` for the segment that contains the page data.
// We only do this on subsequent reads, as otherwise there'd be no loading data to re-use.
// We skip this branch if only the hash fragment has changed, as we don't want to trigger a lazy fetch in that case
applied = triggerLazyFetchForLeafSegments(cache, currentCache, flightSegmentPath, treePatch);
// since we re-used the stale cache's loading state & refreshed the data,
// update the `lastUsedTime` so that it can continue to be re-used for the next 30s
prefetchValues.lastUsedTime = Date.now();
} else {
applied = (0, _applyflightdata.applyFlightData)(currentCache, cache, normalizedFlightData, prefetchValues);
}
const hardNavigate = (0, _shouldhardnavigate.shouldHardNavigate)(// TODO-APP: remove ''
flightSegmentPathWithLeadingEmpty, currentTree);
if (hardNavigate) {
// Copy rsc for the root node of the cache.
cache.rsc = currentCache.rsc;
cache.prefetchRsc = currentCache.prefetchRsc;
(0, _invalidatecachebelowflightsegmentpath.invalidateCacheBelowFlightSegmentPath)(cache, currentCache, flightSegmentPath);
// Ensure the existing cache value is used when the cache was not invalidated.
mutable.cache = cache;
} else if (applied) {
mutable.cache = cache;
// If we applied the cache, we update the "current cache" value so any other
// segments in the FlightDataPath will be able to reference the updated cache.
currentCache = cache;
}
for (const subSegment of generateSegmentsFromPatch(treePatch)){
const scrollableSegmentPath = [
...flightSegmentPath,
...subSegment
];
// Filter out the __DEFAULT__ paths as they shouldn't be scrolled to in this case.
if (scrollableSegmentPath[scrollableSegmentPath.length - 1] !== _segment.DEFAULT_SEGMENT_KEY) {
scrollableSegments.push(scrollableSegmentPath);
}
}
}
currentTree = newTree;
}
}
mutable.patchedTree = currentTree;
mutable.canonicalUrl = updatedCanonicalUrl;
mutable.scrollableSegments = scrollableSegments;
mutable.hashFragment = hash;
mutable.shouldScroll = shouldScroll;
return (0, _handlemutable.handleMutable)(state, mutable);
}, ()=>state);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=navigate-reducer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
import type { PrefetchAction, ReducerState, ReadonlyReducerState } from '../router-reducer-types';
import { PromiseQueue } from '../../promise-queue';
export declare const prefetchQueue: PromiseQueue;
export declare const prefetchReducer: typeof identityReducerWhenSegmentCacheIsEnabled | typeof prefetchReducerImpl;
declare function identityReducerWhenSegmentCacheIsEnabled<T>(state: T): T;
declare function prefetchReducerImpl(state: ReadonlyReducerState, action: PrefetchAction): ReducerState;
export {};

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
prefetchQueue: null,
prefetchReducer: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
prefetchQueue: function() {
return prefetchQueue;
},
prefetchReducer: function() {
return prefetchReducer;
}
});
const _promisequeue = require("../../promise-queue");
const _prefetchcacheutils = require("../prefetch-cache-utils");
const prefetchQueue = new _promisequeue.PromiseQueue(5);
const prefetchReducer = process.env.__NEXT_CLIENT_SEGMENT_CACHE ? identityReducerWhenSegmentCacheIsEnabled : prefetchReducerImpl;
function identityReducerWhenSegmentCacheIsEnabled(state) {
// Unlike the old implementation, the Segment Cache doesn't store its data in
// the router reducer state.
//
// This shouldn't be reachable because we wrap the prefetch API in a check,
// too, which prevents the action from being dispatched. But it's here for
// clarity + code elimination.
return state;
}
function prefetchReducerImpl(state, action) {
// let's prune the prefetch cache before we do anything else
(0, _prefetchcacheutils.prunePrefetchCache)(state.prefetchCache);
const { url } = action;
(0, _prefetchcacheutils.getOrCreatePrefetchCacheEntry)({
url,
nextUrl: state.nextUrl,
prefetchCache: state.prefetchCache,
kind: action.kind,
tree: state.tree,
allowAliasing: true
});
return state;
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=prefetch-reducer.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/client/components/router-reducer/reducers/prefetch-reducer.ts"],"sourcesContent":["import type {\n PrefetchAction,\n ReducerState,\n ReadonlyReducerState,\n} from '../router-reducer-types'\nimport { PromiseQueue } from '../../promise-queue'\nimport {\n getOrCreatePrefetchCacheEntry,\n prunePrefetchCache,\n} from '../prefetch-cache-utils'\nexport const prefetchQueue = new PromiseQueue(5)\n\nexport const prefetchReducer = process.env.__NEXT_CLIENT_SEGMENT_CACHE\n ? identityReducerWhenSegmentCacheIsEnabled\n : prefetchReducerImpl\n\nfunction identityReducerWhenSegmentCacheIsEnabled<T>(state: T): T {\n // Unlike the old implementation, the Segment Cache doesn't store its data in\n // the router reducer state.\n //\n // This shouldn't be reachable because we wrap the prefetch API in a check,\n // too, which prevents the action from being dispatched. But it's here for\n // clarity + code elimination.\n return state\n}\n\nfunction prefetchReducerImpl(\n state: ReadonlyReducerState,\n action: PrefetchAction\n): ReducerState {\n // let's prune the prefetch cache before we do anything else\n prunePrefetchCache(state.prefetchCache)\n\n const { url } = action\n\n getOrCreatePrefetchCacheEntry({\n url,\n nextUrl: state.nextUrl,\n prefetchCache: state.prefetchCache,\n kind: action.kind,\n tree: state.tree,\n allowAliasing: true,\n })\n\n return state\n}\n"],"names":["prefetchQueue","prefetchReducer","PromiseQueue","process","env","__NEXT_CLIENT_SEGMENT_CACHE","identityReducerWhenSegmentCacheIsEnabled","prefetchReducerImpl","state","action","prunePrefetchCache","prefetchCache","url","getOrCreatePrefetchCacheEntry","nextUrl","kind","tree","allowAliasing"],"mappings":";;;;;;;;;;;;;;;IAUaA,aAAa;eAAbA;;IAEAC,eAAe;eAAfA;;;8BAPgB;oCAItB;AACA,MAAMD,gBAAgB,IAAIE,0BAAY,CAAC;AAEvC,MAAMD,kBAAkBE,QAAQC,GAAG,CAACC,2BAA2B,GAClEC,2CACAC;AAEJ,SAASD,yCAA4CE,KAAQ;IAC3D,6EAA6E;IAC7E,4BAA4B;IAC5B,EAAE;IACF,2EAA2E;IAC3E,0EAA0E;IAC1E,8BAA8B;IAC9B,OAAOA;AACT;AAEA,SAASD,oBACPC,KAA2B,EAC3BC,MAAsB;IAEtB,4DAA4D;IAC5DC,IAAAA,sCAAkB,EAACF,MAAMG,aAAa;IAEtC,MAAM,EAAEC,GAAG,EAAE,GAAGH;IAEhBI,IAAAA,iDAA6B,EAAC;QAC5BD;QACAE,SAASN,MAAMM,OAAO;QACtBH,eAAeH,MAAMG,aAAa;QAClCI,MAAMN,OAAOM,IAAI;QACjBC,MAAMR,MAAMQ,IAAI;QAChBC,eAAe;IACjB;IAEA,OAAOT;AACT"}

View File

@@ -0,0 +1,2 @@
import type { ReadonlyReducerState, ReducerState, RefreshAction } from '../router-reducer-types';
export declare function refreshReducer(state: ReadonlyReducerState, action: RefreshAction): ReducerState;

View File

@@ -0,0 +1,109 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "refreshReducer", {
enumerable: true,
get: function() {
return refreshReducer;
}
});
const _fetchserverresponse = require("../fetch-server-response");
const _createhreffromurl = require("../create-href-from-url");
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
const _navigatereducer = require("./navigate-reducer");
const _handlemutable = require("../handle-mutable");
const _filllazyitemstillleafwithhead = require("../fill-lazy-items-till-leaf-with-head");
const _approuter = require("../../app-router");
const _handlesegmentmismatch = require("../handle-segment-mismatch");
const _hasinterceptionrouteincurrenttree = require("./has-interception-route-in-current-tree");
const _refetchinactiveparallelsegments = require("../refetch-inactive-parallel-segments");
const _segmentcache = require("../../segment-cache");
function refreshReducer(state, action) {
const { origin } = action;
const mutable = {};
const href = state.canonicalUrl;
let currentTree = state.tree;
mutable.preserveCustomHistoryState = false;
const cache = (0, _approuter.createEmptyCacheNode)();
// If the current tree was intercepted, the nextUrl should be included in the request.
// This is to ensure that the refresh request doesn't get intercepted, accidentally triggering the interception route.
const includeNextUrl = (0, _hasinterceptionrouteincurrenttree.hasInterceptionRouteInCurrentTree)(state.tree);
// TODO-APP: verify that `href` is not an external url.
// Fetch data from the root of the tree.
cache.lazyData = (0, _fetchserverresponse.fetchServerResponse)(new URL(href, origin), {
flightRouterState: [
currentTree[0],
currentTree[1],
currentTree[2],
'refetch'
],
nextUrl: includeNextUrl ? state.nextUrl : null
});
return cache.lazyData.then(async (param)=>{
let { flightData, canonicalUrl: canonicalUrlOverride } = param;
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, flightData, state.pushRef.pendingPush);
}
// Remove cache.lazyData as it has been resolved at this point.
cache.lazyData = null;
for (const normalizedFlightData of flightData){
const { tree: treePatch, seedData: cacheNodeSeedData, head, isRootRender } = normalizedFlightData;
if (!isRootRender) {
// TODO-APP: handle this case better
console.log('REFRESH FAILED');
return state;
}
const newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
[
''
], currentTree, treePatch, state.canonicalUrl);
if (newTree === null) {
return (0, _handlesegmentmismatch.handleSegmentMismatch)(state, action, treePatch);
}
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, href, state.pushRef.pendingPush);
}
const canonicalUrlOverrideHref = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : undefined;
if (canonicalUrlOverride) {
mutable.canonicalUrl = canonicalUrlOverrideHref;
}
// Handles case where prefetch only returns the router tree patch without rendered components.
if (cacheNodeSeedData !== null) {
const rsc = cacheNodeSeedData[1];
const loading = cacheNodeSeedData[3];
cache.rsc = rsc;
cache.prefetchRsc = null;
cache.loading = loading;
(0, _filllazyitemstillleafwithhead.fillLazyItemsTillLeafWithHead)(cache, // Existing cache is not passed in as `router.refresh()` has to invalidate the entire cache.
undefined, treePatch, cacheNodeSeedData, head, undefined);
if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) {
(0, _segmentcache.revalidateEntireCache)(state.nextUrl, newTree);
} else {
mutable.prefetchCache = new Map();
}
}
await (0, _refetchinactiveparallelsegments.refreshInactiveParallelSegments)({
state,
updatedTree: newTree,
updatedCache: cache,
includeNextUrl,
canonicalUrl: mutable.canonicalUrl || state.canonicalUrl
});
mutable.cache = cache;
mutable.patchedTree = newTree;
currentTree = newTree;
}
return (0, _handlemutable.handleMutable)(state, mutable);
}, ()=>state);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=refresh-reducer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
import type { ReadonlyReducerState, ReducerState, RestoreAction } from '../router-reducer-types';
export declare function restoreReducer(state: ReadonlyReducerState, action: RestoreAction): ReducerState;

View File

@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "restoreReducer", {
enumerable: true,
get: function() {
return restoreReducer;
}
});
const _createhreffromurl = require("../create-href-from-url");
const _computechangedpath = require("../compute-changed-path");
const _pprnavigations = require("../ppr-navigations");
function restoreReducer(state, action) {
const { url, tree } = action;
const href = (0, _createhreffromurl.createHrefFromUrl)(url);
// 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.
const treeToRestore = tree || state.tree;
const oldCache = state.cache;
const newCache = process.env.__NEXT_PPR ? // data for any segment whose dynamic data was already received. This
// prevents an unnecessary flash back to PPR state during a
// back/forward navigation.
(0, _pprnavigations.updateCacheNodeOnPopstateRestoration)(oldCache, treeToRestore) : oldCache;
var _extractPathFromFlightRouterState;
return {
// Set canonical url
canonicalUrl: href,
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: newCache,
prefetchCache: state.prefetchCache,
// Restore provided tree
tree: treeToRestore,
nextUrl: (_extractPathFromFlightRouterState = (0, _computechangedpath.extractPathFromFlightRouterState)(treeToRestore)) != null ? _extractPathFromFlightRouterState : url.pathname
};
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=restore-reducer.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/client/components/router-reducer/reducers/restore-reducer.ts"],"sourcesContent":["import { createHrefFromUrl } from '../create-href-from-url'\nimport type {\n ReadonlyReducerState,\n ReducerState,\n RestoreAction,\n} from '../router-reducer-types'\nimport { extractPathFromFlightRouterState } from '../compute-changed-path'\nimport { updateCacheNodeOnPopstateRestoration } from '../ppr-navigations'\n\nexport function restoreReducer(\n state: ReadonlyReducerState,\n action: RestoreAction\n): ReducerState {\n const { url, tree } = action\n const href = createHrefFromUrl(url)\n // This action is used to restore the router state from the history state.\n // However, it's possible that the history state no longer contains the `FlightRouterState`.\n // We will copy over the internal state on pushState/replaceState events, but if a history entry\n // occurred before hydration, or if the user navigated to a hash using a regular anchor link,\n // the history state will not contain the `FlightRouterState`.\n // In this case, we'll continue to use the existing tree so the router doesn't get into an invalid state.\n const treeToRestore = tree || state.tree\n\n const oldCache = state.cache\n const newCache = process.env.__NEXT_PPR\n ? // When PPR is enabled, we update the cache to drop the prefetch\n // data for any segment whose dynamic data was already received. This\n // prevents an unnecessary flash back to PPR state during a\n // back/forward navigation.\n updateCacheNodeOnPopstateRestoration(oldCache, treeToRestore)\n : oldCache\n\n return {\n // Set canonical url\n canonicalUrl: href,\n pushRef: {\n pendingPush: false,\n mpaNavigation: false,\n // Ensures that the custom history state that was set is preserved when applying this update.\n preserveCustomHistoryState: true,\n },\n focusAndScrollRef: state.focusAndScrollRef,\n cache: newCache,\n prefetchCache: state.prefetchCache,\n // Restore provided tree\n tree: treeToRestore,\n nextUrl: extractPathFromFlightRouterState(treeToRestore) ?? url.pathname,\n }\n}\n"],"names":["restoreReducer","state","action","url","tree","href","createHrefFromUrl","treeToRestore","oldCache","cache","newCache","process","env","__NEXT_PPR","updateCacheNodeOnPopstateRestoration","extractPathFromFlightRouterState","canonicalUrl","pushRef","pendingPush","mpaNavigation","preserveCustomHistoryState","focusAndScrollRef","prefetchCache","nextUrl","pathname"],"mappings":";;;;+BASgBA;;;eAAAA;;;mCATkB;oCAMe;gCACI;AAE9C,SAASA,eACdC,KAA2B,EAC3BC,MAAqB;IAErB,MAAM,EAAEC,GAAG,EAAEC,IAAI,EAAE,GAAGF;IACtB,MAAMG,OAAOC,IAAAA,oCAAiB,EAACH;IAC/B,0EAA0E;IAC1E,4FAA4F;IAC5F,gGAAgG;IAChG,6FAA6F;IAC7F,8DAA8D;IAC9D,yGAAyG;IACzG,MAAMI,gBAAgBH,QAAQH,MAAMG,IAAI;IAExC,MAAMI,WAAWP,MAAMQ,KAAK;IAC5B,MAAMC,WAAWC,QAAQC,GAAG,CAACC,UAAU,GAEnC,qEAAqE;IACrE,2DAA2D;IAC3D,2BAA2B;IAC3BC,IAAAA,oDAAoC,EAACN,UAAUD,iBAC/CC;QAgBOO;IAdX,OAAO;QACL,oBAAoB;QACpBC,cAAcX;QACdY,SAAS;YACPC,aAAa;YACbC,eAAe;YACf,6FAA6F;YAC7FC,4BAA4B;QAC9B;QACAC,mBAAmBpB,MAAMoB,iBAAiB;QAC1CZ,OAAOC;QACPY,eAAerB,MAAMqB,aAAa;QAClC,wBAAwB;QACxBlB,MAAMG;QACNgB,SAASR,CAAAA,oCAAAA,IAAAA,oDAAgC,EAACR,0BAAjCQ,oCAAmDZ,IAAIqB,QAAQ;IAC1E;AACF"}

View File

@@ -0,0 +1,2 @@
import { type ReadonlyReducerState, type ReducerState, type ServerActionAction } from '../router-reducer-types';
export declare function serverActionReducer(state: ReadonlyReducerState, action: ServerActionAction): ReducerState;

View File

@@ -0,0 +1,282 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "serverActionReducer", {
enumerable: true,
get: function() {
return serverActionReducer;
}
});
const _appcallserver = require("../../../app-call-server");
const _appfindsourcemapurl = require("../../../app-find-source-map-url");
const _approuterheaders = require("../../app-router-headers");
const _routerreducertypes = require("../router-reducer-types");
const _assignlocation = require("../../../assign-location");
const _createhreffromurl = require("../create-href-from-url");
const _navigatereducer = require("./navigate-reducer");
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
const _handlemutable = require("../handle-mutable");
const _filllazyitemstillleafwithhead = require("../fill-lazy-items-till-leaf-with-head");
const _approuter = require("../../app-router");
const _hasinterceptionrouteincurrenttree = require("./has-interception-route-in-current-tree");
const _handlesegmentmismatch = require("../handle-segment-mismatch");
const _refetchinactiveparallelsegments = require("../refetch-inactive-parallel-segments");
const _flightdatahelpers = require("../../../flight-data-helpers");
const _redirect = require("../../redirect");
const _redirecterror = require("../../redirect-error");
const _prefetchcacheutils = require("../prefetch-cache-utils");
const _removebasepath = require("../../../remove-base-path");
const _hasbasepath = require("../../../has-base-path");
const _serverreferenceinfo = require("../../../../shared/lib/server-reference-info");
const _segmentcache = require("../../segment-cache");
// // eslint-disable-next-line import/no-extraneous-dependencies
// import { createFromFetch } from 'react-server-dom-webpack/client'
// // eslint-disable-next-line import/no-extraneous-dependencies
// import { encodeReply } from 'react-server-dom-webpack/client'
const { createFromFetch, createTemporaryReferenceSet, encodeReply } = !!process.env.NEXT_RUNTIME ? require('react-server-dom-webpack/client.edge') : require('react-server-dom-webpack/client');
async function fetchServerAction(state, nextUrl, param) {
let { actionId, actionArgs } = param;
const temporaryReferences = createTemporaryReferenceSet();
const info = (0, _serverreferenceinfo.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' ? (0, _serverreferenceinfo.omitUnusedArgs)(actionArgs, info) : actionArgs;
const body = await encodeReply(usedArgs, {
temporaryReferences
});
const res = await fetch('', {
method: 'POST',
headers: {
Accept: _approuterheaders.RSC_CONTENT_TYPE_HEADER,
[_approuterheaders.ACTION_HEADER]: actionId,
[_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(JSON.stringify(state.tree)),
...process.env.NEXT_DEPLOYMENT_ID ? {
'x-deployment-id': process.env.NEXT_DEPLOYMENT_ID
} : {},
...nextUrl ? {
[_approuterheaders.NEXT_URL]: nextUrl
} : {}
},
body
});
const redirectHeader = res.headers.get('x-action-redirect');
const [location, _redirectType] = (redirectHeader == null ? void 0 : redirectHeader.split(';')) || [];
let redirectType;
switch(_redirectType){
case 'push':
redirectType = _redirecterror.RedirectType.push;
break;
case 'replace':
redirectType = _redirecterror.RedirectType.replace;
break;
default:
redirectType = undefined;
}
const isPrerender = !!res.headers.get(_approuterheaders.NEXT_IS_PRERENDER_HEADER);
let revalidatedParts;
try {
const revalidatedHeader = JSON.parse(res.headers.get('x-action-revalidated') || '[[],0,0]');
revalidatedParts = {
paths: revalidatedHeader[0] || [],
tag: !!revalidatedHeader[1],
cookie: revalidatedHeader[2]
};
} catch (e) {
revalidatedParts = {
paths: [],
tag: false,
cookie: false
};
}
const redirectLocation = location ? (0, _assignlocation.assignLocation)(location, new URL(state.canonicalUrl, window.location.href)) : undefined;
const contentType = res.headers.get('content-type');
if (contentType == null ? void 0 : contentType.startsWith(_approuterheaders.RSC_CONTENT_TYPE_HEADER)) {
const response = await createFromFetch(Promise.resolve(res), {
callServer: _appcallserver.callServer,
findSourceMapURL: _appfindsourcemapurl.findSourceMapURL,
temporaryReferences
});
if (location) {
// if it was a redirection, then result is just a regular RSC payload
return {
actionFlightData: (0, _flightdatahelpers.normalizeFlightData)(response.f),
redirectLocation,
redirectType,
revalidatedParts,
isPrerender
};
}
return {
actionResult: response.a,
actionFlightData: (0, _flightdatahelpers.normalizeFlightData)(response.f),
redirectLocation,
redirectType,
revalidatedParts,
isPrerender
};
}
// Handle invalid server action responses
if (res.status >= 400) {
// The server can respond with a text/plain error message, but we'll fallback to something generic
// if there isn't one.
const error = contentType === 'text/plain' ? await res.text() : 'An unexpected response was received from the server.';
throw Object.defineProperty(new Error(error), "__NEXT_ERROR_CODE", {
value: "E394",
enumerable: false,
configurable: true
});
}
return {
redirectLocation,
redirectType,
revalidatedParts,
isPrerender
};
}
function serverActionReducer(state, action) {
const { resolve, reject } = action;
const mutable = {};
let currentTree = state.tree;
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 = state.nextUrl && (0, _hasinterceptionrouteincurrenttree.hasInterceptionRouteInCurrentTree)(state.tree) ? state.nextUrl : null;
return fetchServerAction(state, nextUrl, action).then(async (param)=>{
let { actionResult, actionFlightData: flightData, redirectLocation, redirectType, isPrerender, revalidatedParts } = param;
let redirectHref;
// honor the redirect type instead of defaulting to push in case of server actions.
if (redirectLocation) {
if (redirectType === _redirecterror.RedirectType.replace) {
state.pushRef.pendingPush = false;
mutable.pendingPush = false;
} else {
state.pushRef.pendingPush = true;
mutable.pendingPush = true;
}
redirectHref = (0, _createhreffromurl.createHrefFromUrl)(redirectLocation, false);
mutable.canonicalUrl = redirectHref;
}
if (!flightData) {
resolve(actionResult);
// If there is a redirect but no flight data we need to do a mpaNavigation.
if (redirectLocation) {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, redirectLocation.href, state.pushRef.pendingPush);
}
return state;
}
if (typeof flightData === 'string') {
// Handle case when navigating to page in `pages` from `app`
resolve(actionResult);
return (0, _navigatereducer.handleExternalUrl)(state, mutable, flightData, state.pushRef.pendingPush);
}
const actionRevalidated = revalidatedParts.paths.length > 0 || revalidatedParts.tag || revalidatedParts.cookie;
for (const normalizedFlightData of flightData){
const { tree: treePatch, seedData: cacheNodeSeedData, head, isRootRender } = normalizedFlightData;
if (!isRootRender) {
// TODO-APP: handle this case better
console.log('SERVER ACTION APPLY FAILED');
resolve(actionResult);
return state;
}
// Given the path can only have two items the items are only the router state and rsc for the root.
const newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
[
''
], currentTree, treePatch, redirectHref ? redirectHref : state.canonicalUrl);
if (newTree === null) {
resolve(actionResult);
return (0, _handlesegmentmismatch.handleSegmentMismatch)(state, action, treePatch);
}
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
resolve(actionResult);
return (0, _navigatereducer.handleExternalUrl)(state, mutable, redirectHref || state.canonicalUrl, state.pushRef.pendingPush);
}
// The server sent back RSC data for the server action, so we need to apply it to the cache.
if (cacheNodeSeedData !== null) {
const rsc = cacheNodeSeedData[1];
const cache = (0, _approuter.createEmptyCacheNode)();
cache.rsc = rsc;
cache.prefetchRsc = null;
cache.loading = cacheNodeSeedData[3];
(0, _filllazyitemstillleafwithhead.fillLazyItemsTillLeafWithHead)(cache, // Existing cache is not passed in as server actions have to invalidate the entire cache.
undefined, treePatch, cacheNodeSeedData, head, undefined);
mutable.cache = cache;
if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) {
(0, _segmentcache.revalidateEntireCache)(state.nextUrl, newTree);
} else {
mutable.prefetchCache = new Map();
}
if (actionRevalidated) {
await (0, _refetchinactiveparallelsegments.refreshInactiveParallelSegments)({
state,
updatedTree: newTree,
updatedCache: cache,
includeNextUrl: Boolean(nextUrl),
canonicalUrl: mutable.canonicalUrl || state.canonicalUrl
});
}
}
mutable.patchedTree = newTree;
currentTree = newTree;
}
if (redirectLocation && redirectHref) {
if (!process.env.__NEXT_CLIENT_SEGMENT_CACHE && !actionRevalidated) {
// Because the RedirectBoundary will trigger a navigation, we need to seed the prefetch cache
// with the FlightData that we got from the server action for the target page, so that it's
// available when the page is navigated to and doesn't need to be re-fetched.
// We only do this if the server action didn't revalidate any data, as in that case the
// client cache will be cleared and the data will be re-fetched anyway.
// NOTE: We don't do this in the Segment Cache implementation.
// Dynamic data should never be placed into the cache, unless it's
// "converted" to static data using <Link prefetch={true}>. What we
// do instead is re-prefetch links and forms whenever the cache is
// invalidated.
(0, _prefetchcacheutils.createSeededPrefetchCacheEntry)({
url: redirectLocation,
data: {
flightData,
canonicalUrl: undefined,
couldBeIntercepted: false,
prerendered: false,
postponed: false,
// TODO: We should be able to set this if the server action
// returned a fully static response.
staleTime: -1
},
tree: state.tree,
prefetchCache: state.prefetchCache,
nextUrl: state.nextUrl,
kind: isPrerender ? _routerreducertypes.PrefetchKind.FULL : _routerreducertypes.PrefetchKind.AUTO
});
mutable.prefetchCache = state.prefetchCache;
}
// 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.
reject((0, _redirect.getRedirectError)((0, _hasbasepath.hasBasePath)(redirectHref) ? (0, _removebasepath.removeBasePath)(redirectHref) : redirectHref, redirectType || _redirecterror.RedirectType.push));
} else {
resolve(actionResult);
}
return (0, _handlemutable.handleMutable)(state, mutable);
}, (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;
});
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=server-action-reducer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
import type { ServerPatchAction, ReducerState, ReadonlyReducerState } from '../router-reducer-types';
export declare function serverPatchReducer(state: ReadonlyReducerState, action: ServerPatchAction): ReducerState;

View File

@@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "serverPatchReducer", {
enumerable: true,
get: function() {
return serverPatchReducer;
}
});
const _createhreffromurl = require("../create-href-from-url");
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
const _navigatereducer = require("./navigate-reducer");
const _applyflightdata = require("../apply-flight-data");
const _handlemutable = require("../handle-mutable");
const _approuter = require("../../app-router");
function serverPatchReducer(state, action) {
const { serverResponse: { flightData, canonicalUrl: canonicalUrlOverride } } = action;
const mutable = {};
mutable.preserveCustomHistoryState = false;
// Handle case when navigating to page in `pages` from `app`
if (typeof flightData === 'string') {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, flightData, state.pushRef.pendingPush);
}
let currentTree = state.tree;
let currentCache = state.cache;
for (const normalizedFlightData of flightData){
const { segmentPath: flightSegmentPath, tree: treePatch } = normalizedFlightData;
const newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
[
'',
...flightSegmentPath
], currentTree, treePatch, state.canonicalUrl);
// `applyRouterStatePatchToTree` returns `null` when it determined that the server response is not applicable to the current tree.
// In other words, the server responded with a tree that doesn't match what the client is currently rendering.
// This can happen if the server patch action took longer to resolve than a subsequent navigation which would have changed the tree.
// Previously this case triggered an MPA navigation but it should be safe to simply discard the server response rather than forcing
// the entire page to reload.
if (newTree === null) {
return state;
}
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
return (0, _navigatereducer.handleExternalUrl)(state, mutable, state.canonicalUrl, state.pushRef.pendingPush);
}
const canonicalUrlOverrideHref = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : undefined;
if (canonicalUrlOverrideHref) {
mutable.canonicalUrl = canonicalUrlOverrideHref;
}
const cache = (0, _approuter.createEmptyCacheNode)();
(0, _applyflightdata.applyFlightData)(currentCache, cache, normalizedFlightData);
mutable.patchedTree = newTree;
mutable.cache = cache;
currentCache = cache;
currentTree = newTree;
}
return (0, _handlemutable.handleMutable)(state, mutable);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=server-patch-reducer.js.map

View File

@@ -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 { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'\nimport { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout'\nimport type {\n ServerPatchAction,\n ReducerState,\n ReadonlyReducerState,\n Mutable,\n} from '../router-reducer-types'\nimport { handleExternalUrl } from './navigate-reducer'\nimport { applyFlightData } from '../apply-flight-data'\nimport { handleMutable } from '../handle-mutable'\nimport type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime'\nimport { createEmptyCacheNode } from '../../app-router'\n\nexport function serverPatchReducer(\n state: ReadonlyReducerState,\n action: ServerPatchAction\n): ReducerState {\n const {\n serverResponse: { flightData, canonicalUrl: canonicalUrlOverride },\n } = action\n\n const mutable: Mutable = {}\n\n mutable.preserveCustomHistoryState = false\n\n // Handle case when navigating to page in `pages` from `app`\n if (typeof flightData === 'string') {\n return handleExternalUrl(\n state,\n mutable,\n flightData,\n state.pushRef.pendingPush\n )\n }\n\n let currentTree = state.tree\n let currentCache = state.cache\n\n for (const normalizedFlightData of flightData) {\n const { segmentPath: flightSegmentPath, tree: treePatch } =\n normalizedFlightData\n\n const newTree = applyRouterStatePatchToTree(\n // TODO-APP: remove ''\n ['', ...flightSegmentPath],\n currentTree,\n treePatch,\n state.canonicalUrl\n )\n\n // `applyRouterStatePatchToTree` returns `null` when it determined that the server response is not applicable to the current tree.\n // In other words, the server responded with a tree that doesn't match what the client is currently rendering.\n // This can happen if the server patch action took longer to resolve than a subsequent navigation which would have changed the tree.\n // Previously this case triggered an MPA navigation but it should be safe to simply discard the server response rather than forcing\n // the entire page to reload.\n if (newTree === null) {\n return state\n }\n\n if (isNavigatingToNewRootLayout(currentTree, newTree)) {\n return handleExternalUrl(\n state,\n mutable,\n state.canonicalUrl,\n state.pushRef.pendingPush\n )\n }\n\n const canonicalUrlOverrideHref = canonicalUrlOverride\n ? createHrefFromUrl(canonicalUrlOverride)\n : undefined\n\n if (canonicalUrlOverrideHref) {\n mutable.canonicalUrl = canonicalUrlOverrideHref\n }\n\n const cache: CacheNode = createEmptyCacheNode()\n applyFlightData(currentCache, cache, normalizedFlightData)\n\n mutable.patchedTree = newTree\n mutable.cache = cache\n\n currentCache = cache\n currentTree = newTree\n }\n\n return handleMutable(state, mutable)\n}\n"],"names":["serverPatchReducer","state","action","serverResponse","flightData","canonicalUrl","canonicalUrlOverride","mutable","preserveCustomHistoryState","handleExternalUrl","pushRef","pendingPush","currentTree","tree","currentCache","cache","normalizedFlightData","segmentPath","flightSegmentPath","treePatch","newTree","applyRouterStatePatchToTree","isNavigatingToNewRootLayout","canonicalUrlOverrideHref","createHrefFromUrl","undefined","createEmptyCacheNode","applyFlightData","patchedTree","handleMutable"],"mappings":";;;;+BAegBA;;;eAAAA;;;mCAfkB;6CACU;6CACA;iCAOV;iCACF;+BACF;2BAEO;AAE9B,SAASA,mBACdC,KAA2B,EAC3BC,MAAyB;IAEzB,MAAM,EACJC,gBAAgB,EAAEC,UAAU,EAAEC,cAAcC,oBAAoB,EAAE,EACnE,GAAGJ;IAEJ,MAAMK,UAAmB,CAAC;IAE1BA,QAAQC,0BAA0B,GAAG;IAErC,4DAA4D;IAC5D,IAAI,OAAOJ,eAAe,UAAU;QAClC,OAAOK,IAAAA,kCAAiB,EACtBR,OACAM,SACAH,YACAH,MAAMS,OAAO,CAACC,WAAW;IAE7B;IAEA,IAAIC,cAAcX,MAAMY,IAAI;IAC5B,IAAIC,eAAeb,MAAMc,KAAK;IAE9B,KAAK,MAAMC,wBAAwBZ,WAAY;QAC7C,MAAM,EAAEa,aAAaC,iBAAiB,EAAEL,MAAMM,SAAS,EAAE,GACvDH;QAEF,MAAMI,UAAUC,IAAAA,wDAA2B,EACzC,sBAAsB;QACtB;YAAC;eAAOH;SAAkB,EAC1BN,aACAO,WACAlB,MAAMI,YAAY;QAGpB,kIAAkI;QAClI,8GAA8G;QAC9G,oIAAoI;QACpI,mIAAmI;QACnI,6BAA6B;QAC7B,IAAIe,YAAY,MAAM;YACpB,OAAOnB;QACT;QAEA,IAAIqB,IAAAA,wDAA2B,EAACV,aAAaQ,UAAU;YACrD,OAAOX,IAAAA,kCAAiB,EACtBR,OACAM,SACAN,MAAMI,YAAY,EAClBJ,MAAMS,OAAO,CAACC,WAAW;QAE7B;QAEA,MAAMY,2BAA2BjB,uBAC7BkB,IAAAA,oCAAiB,EAAClB,wBAClBmB;QAEJ,IAAIF,0BAA0B;YAC5BhB,QAAQF,YAAY,GAAGkB;QACzB;QAEA,MAAMR,QAAmBW,IAAAA,+BAAoB;QAC7CC,IAAAA,gCAAe,EAACb,cAAcC,OAAOC;QAErCT,QAAQqB,WAAW,GAAGR;QACtBb,QAAQQ,KAAK,GAAGA;QAEhBD,eAAeC;QACfH,cAAcQ;IAChB;IAEA,OAAOS,IAAAA,4BAAa,EAAC5B,OAAOM;AAC9B"}

View File

@@ -0,0 +1,30 @@
import type { FlightRouterState } from '../../../server/app-render/types';
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { AppRouterState } from './router-reducer-types';
interface RefreshInactiveParallelSegments {
state: AppRouterState;
updatedTree: FlightRouterState;
updatedCache: CacheNode;
includeNextUrl: boolean;
canonicalUrl: string;
}
/**
* Refreshes inactive segments that are still in the current FlightRouterState.
* A segment is considered "inactive" when the server response indicates it didn't match to a page component.
* This happens during a soft-navigation, where the server will want to patch in the segment
* with the "default" component, but we explicitly ignore the server in this case
* and keep the existing state for that segment. New data for inactive segments are inherently
* not part of the server response when we patch the tree, because they were associated with a response
* from an earlier navigation/request. For each segment, once it becomes "active", we encode the URL that provided
* the data for it. This function traverses parallel routes looking for these markers so that it can re-fetch
* and patch the new data into the tree.
*/
export declare function refreshInactiveParallelSegments(options: RefreshInactiveParallelSegments): Promise<void>;
/**
* Walks the current parallel segments to determine if they are "active".
* An active parallel route will have a `__PAGE__` segment in the FlightRouterState.
* As opposed to a `__DEFAULT__` segment, which means there was no match for that parallel route.
* We add a special marker here so that we know how to refresh its data when the router is revalidated.
*/
export declare function addRefreshMarkerToActiveParallelSegments(tree: FlightRouterState, path: string): void;
export {};

View File

@@ -0,0 +1,104 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
addRefreshMarkerToActiveParallelSegments: null,
refreshInactiveParallelSegments: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
addRefreshMarkerToActiveParallelSegments: function() {
return addRefreshMarkerToActiveParallelSegments;
},
refreshInactiveParallelSegments: function() {
return refreshInactiveParallelSegments;
}
});
const _applyflightdata = require("./apply-flight-data");
const _fetchserverresponse = require("./fetch-server-response");
const _segment = require("../../../shared/lib/segment");
async function refreshInactiveParallelSegments(options) {
const fetchedSegments = new Set();
await refreshInactiveParallelSegmentsImpl({
...options,
rootTree: options.updatedTree,
fetchedSegments
});
}
async function refreshInactiveParallelSegmentsImpl(param) {
let { state, updatedTree, updatedCache, includeNextUrl, fetchedSegments, rootTree = updatedTree, canonicalUrl } = param;
const [, parallelRoutes, refetchPath, refetchMarker] = updatedTree;
const fetchPromises = [];
if (refetchPath && refetchPath !== canonicalUrl && refetchMarker === 'refresh' && // it's possible for the tree to contain multiple segments that contain data at the same URL
// we keep track of them so we can dedupe the requests
!fetchedSegments.has(refetchPath)) {
fetchedSegments.add(refetchPath) // Mark this URL as fetched
;
// Eagerly kick off the fetch for the refetch path & the parallel routes. This should be fine to do as they each operate
// independently on their own cache nodes, and `applyFlightData` will copy anything it doesn't care about from the existing cache.
const fetchPromise = (0, _fetchserverresponse.fetchServerResponse)(new URL(refetchPath, location.origin), {
// refetch from the root of the updated tree, otherwise it will be scoped to the current segment
// and might not contain the data we need to patch in interception route data (such as dynamic params from a previous segment)
flightRouterState: [
rootTree[0],
rootTree[1],
rootTree[2],
'refetch'
],
nextUrl: includeNextUrl ? state.nextUrl : null
}).then((param)=>{
let { flightData } = param;
if (typeof flightData !== 'string') {
for (const flightDataPath of flightData){
// we only pass the new cache as this function is called after clearing the router cache
// and filling in the new page data from the server. Meaning the existing cache is actually the cache that's
// just been created & has been written to, but hasn't been "committed" yet.
(0, _applyflightdata.applyFlightData)(updatedCache, updatedCache, flightDataPath);
}
} else {
// When flightData is a string, it suggests that the server response should have triggered an MPA navigation
// I'm not 100% sure of this decision, but it seems unlikely that we'd want to introduce a redirect side effect
// when refreshing on-screen data, so handling this has been ommitted.
}
});
fetchPromises.push(fetchPromise);
}
for(const key in parallelRoutes){
const parallelFetchPromise = refreshInactiveParallelSegmentsImpl({
state,
updatedTree: parallelRoutes[key],
updatedCache,
includeNextUrl,
fetchedSegments,
rootTree,
canonicalUrl
});
fetchPromises.push(parallelFetchPromise);
}
await Promise.all(fetchPromises);
}
function addRefreshMarkerToActiveParallelSegments(tree, path) {
const [segment, parallelRoutes, , refetchMarker] = tree;
// a page segment might also contain concatenated search params, so we do a partial match on the key
if (segment.includes(_segment.PAGE_SEGMENT_KEY) && refetchMarker !== 'refresh') {
tree[2] = path;
tree[3] = 'refresh';
}
for(const key in parallelRoutes){
addRefreshMarkerToActiveParallelSegments(parallelRoutes[key], path);
}
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=refetch-inactive-parallel-segments.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,227 @@
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime';
import type { FlightRouterState, FlightSegmentPath } from '../../../server/app-render/types';
import type { FetchServerResponseResult } from './fetch-server-response';
export declare const ACTION_REFRESH = "refresh";
export declare const ACTION_NAVIGATE = "navigate";
export declare const ACTION_RESTORE = "restore";
export declare const ACTION_SERVER_PATCH = "server-patch";
export declare const ACTION_PREFETCH = "prefetch";
export declare const ACTION_HMR_REFRESH = "hmr-refresh";
export declare const ACTION_SERVER_ACTION = "server-action";
export type RouterChangeByServerResponse = ({ previousTree, serverResponse, }: {
previousTree: FlightRouterState;
serverResponse: FetchServerResponseResult;
}) => void;
export type RouterNavigate = (href: string, navigateType: 'push' | 'replace', shouldScroll: boolean) => void;
export interface Mutable {
mpaNavigation?: boolean;
patchedTree?: FlightRouterState;
canonicalUrl?: string;
scrollableSegments?: FlightSegmentPath[];
pendingPush?: boolean;
cache?: CacheNode;
prefetchCache?: AppRouterState['prefetchCache'];
hashFragment?: string;
shouldScroll?: boolean;
preserveCustomHistoryState?: boolean;
onlyHashChange?: boolean;
}
export interface ServerActionMutable extends Mutable {
inFlightServerAction?: Promise<any> | null;
}
/**
* Refresh triggers a refresh of the full page data.
* - fetches the Flight data and fills rsc at the root of the cache.
* - The router state is updated at the root.
*/
export interface RefreshAction {
type: typeof ACTION_REFRESH;
origin: Location['origin'];
}
export interface HmrRefreshAction {
type: typeof ACTION_HMR_REFRESH;
origin: Location['origin'];
}
export type ServerActionDispatcher = (args: Omit<ServerActionAction, 'type' | 'mutable' | 'navigate' | 'changeByServerResponse' | 'cache'>) => void;
export interface ServerActionAction {
type: typeof ACTION_SERVER_ACTION;
actionId: string;
actionArgs: any[];
resolve: (value: any) => void;
reject: (reason?: any) => void;
}
/**
* Navigate triggers a navigation to the provided url. It supports two types: `push` and `replace`.
*
* `navigateType`:
* - `push` - pushes a new history entry in the browser history
* - `replace` - replaces the current history entry in the browser history
*
* Navigate has multiple cache heuristics:
* - page was prefetched
* - Apply router state tree from prefetch
* - Apply Flight data from prefetch to the cache
* - If Flight data is a string, it's a redirect and the state is updated to trigger a redirect
* - Check if hard navigation is needed
* - Hard navigation happens when a dynamic parameter below the common layout changed
* - When hard navigation is needed the cache is invalidated below the flightSegmentPath
* - The missing cache nodes of the page will be fetched in layout-router and trigger the SERVER_PATCH action
* - If hard navigation is not needed
* - The cache is reused
* - If any cache nodes are missing they'll be fetched in layout-router and trigger the SERVER_PATCH action
* - page was not prefetched
* - The navigate was called from `next/router` (`router.push()` / `router.replace()`) / `next/link` without prefetched data available (e.g. the prefetch didn't come back from the server before clicking the link)
* - Flight data is fetched in the reducer (suspends the reducer)
* - Router state tree is created based on Flight data
* - Cache is filled based on the Flight data
*
* Above steps explain 3 cases:
* - `soft` - Reuses the existing cache and fetches missing nodes in layout-router.
* - `hard` - Creates a new cache where cache nodes are removed below the common layout and fetches missing nodes in layout-router.
* - `optimistic` (explicit no prefetch) - Creates a new cache and kicks off the data fetch in the reducer. The data fetch is awaited in the layout-router.
*/
export interface NavigateAction {
type: typeof ACTION_NAVIGATE;
url: URL;
isExternalUrl: boolean;
locationSearch: Location['search'];
navigateType: 'push' | 'replace';
shouldScroll: boolean;
allowAliasing: boolean;
}
/**
* Restore applies the provided router state.
* - Used for `popstate` (back/forward navigation) where a known router state has to be applied.
* - Also used when syncing the router state with `pushState`/`replaceState` calls.
* - Router state is applied as-is from the history state, if available.
* - If the history state does not contain the router state, the existing router state is used.
* - If any cache node is missing it will be fetched in layout-router during rendering and the server-patch case.
* - If existing cache nodes match these are used.
*/
export interface RestoreAction {
type: typeof ACTION_RESTORE;
url: URL;
tree: FlightRouterState | undefined;
}
/**
* Server-patch applies the provided Flight data to the cache and router tree.
* - Only triggered in layout-router.
* - Creates a new cache and router state with the Flight data applied.
*/
export interface ServerPatchAction {
type: typeof ACTION_SERVER_PATCH;
serverResponse: FetchServerResponseResult;
previousTree: FlightRouterState;
}
/**
* 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.
* - `temporary` - a temporary prefetch entry is added to the cache, this is used when prefetch={false} is used in next/link or when you push a route programmatically.
*/
export declare enum PrefetchKind {
AUTO = "auto",
FULL = "full",
TEMPORARY = "temporary"
}
/**
* Prefetch adds the provided FlightData to the prefetch cache
* - Creates the router state tree based on the patch in FlightData
* - Adds the FlightData to the prefetch cache
* - In ACTION_NAVIGATE the prefetch cache is checked and the router state tree and FlightData are applied.
*/
export interface PrefetchAction {
type: typeof ACTION_PREFETCH;
url: URL;
kind: PrefetchKind;
}
export interface PushRef {
/**
* If the app-router should push a new history entry in app-router's useEffect()
*/
pendingPush: boolean;
/**
* Multi-page navigation through location.href.
*/
mpaNavigation: boolean;
/**
* Skip applying the router state to the browser history state.
*/
preserveCustomHistoryState: boolean;
}
export type FocusAndScrollRef = {
/**
* If focus and scroll should be set in the layout-router's useEffect()
*/
apply: boolean;
/**
* The hash fragment that should be scrolled to.
*/
hashFragment: string | null;
/**
* The paths of the segments that should be focused.
*/
segmentPaths: FlightSegmentPath[];
/**
* If only the URLs hash fragment changed
*/
onlyHashChange: boolean;
};
export type PrefetchCacheEntry = {
treeAtTimeOfPrefetch: FlightRouterState;
data: Promise<FetchServerResponseResult>;
kind: PrefetchKind;
prefetchTime: number;
staleTime: number;
lastUsedTime: number | null;
key: string;
status: PrefetchCacheEntryStatus;
url: URL;
};
export declare enum PrefetchCacheEntryStatus {
fresh = "fresh",
reusable = "reusable",
expired = "expired",
stale = "stale"
}
/**
* Handles keeping the state of app-router.
*/
export type AppRouterState = {
/**
* The router state, this is written into the history state in app-router using replaceState/pushState.
* - Has to be serializable as it is written into the history state.
* - Holds which segments and parallel routes are shown on the screen.
*/
tree: FlightRouterState;
/**
* The cache holds React nodes for every segment that is shown on screen as well as previously shown segments.
* It also holds in-progress data requests.
* Prefetched data is stored separately in `prefetchCache`, that is applied during ACTION_NAVIGATE.
*/
cache: CacheNode;
/**
* Cache that holds prefetched Flight responses keyed by url.
*/
prefetchCache: Map<string, PrefetchCacheEntry>;
/**
* Decides if the update should create a new history entry and if the navigation has to trigger a browser navigation.
*/
pushRef: PushRef;
/**
* Decides if the update should apply scroll and focus management.
*/
focusAndScrollRef: FocusAndScrollRef;
/**
* The canonical url that is pushed/replaced.
* - This is the url you see in the browser.
*/
canonicalUrl: string;
/**
* The underlying "url" representing the UI state, which is used for intercepting routes.
*/
nextUrl: string | null;
};
export type ReadonlyReducerState = Readonly<AppRouterState>;
export type ReducerState = Promise<AppRouterState> | AppRouterState;
export type ReducerActions = Readonly<RefreshAction | NavigateAction | RestoreAction | ServerPatchAction | PrefetchAction | HmrRefreshAction | ServerActionAction>;

View File

@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
ACTION_HMR_REFRESH: null,
ACTION_NAVIGATE: null,
ACTION_PREFETCH: null,
ACTION_REFRESH: null,
ACTION_RESTORE: null,
ACTION_SERVER_ACTION: null,
ACTION_SERVER_PATCH: null,
PrefetchCacheEntryStatus: null,
PrefetchKind: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
ACTION_HMR_REFRESH: function() {
return ACTION_HMR_REFRESH;
},
ACTION_NAVIGATE: function() {
return ACTION_NAVIGATE;
},
ACTION_PREFETCH: function() {
return ACTION_PREFETCH;
},
ACTION_REFRESH: function() {
return ACTION_REFRESH;
},
ACTION_RESTORE: function() {
return ACTION_RESTORE;
},
ACTION_SERVER_ACTION: function() {
return ACTION_SERVER_ACTION;
},
ACTION_SERVER_PATCH: function() {
return ACTION_SERVER_PATCH;
},
PrefetchCacheEntryStatus: function() {
return PrefetchCacheEntryStatus;
},
PrefetchKind: function() {
return PrefetchKind;
}
});
const ACTION_REFRESH = 'refresh';
const ACTION_NAVIGATE = 'navigate';
const ACTION_RESTORE = 'restore';
const ACTION_SERVER_PATCH = 'server-patch';
const ACTION_PREFETCH = 'prefetch';
const ACTION_HMR_REFRESH = 'hmr-refresh';
const ACTION_SERVER_ACTION = 'server-action';
var PrefetchKind = /*#__PURE__*/ function(PrefetchKind) {
PrefetchKind["AUTO"] = "auto";
PrefetchKind["FULL"] = "full";
PrefetchKind["TEMPORARY"] = "temporary";
return PrefetchKind;
}({});
var PrefetchCacheEntryStatus = /*#__PURE__*/ function(PrefetchCacheEntryStatus) {
PrefetchCacheEntryStatus["fresh"] = "fresh";
PrefetchCacheEntryStatus["reusable"] = "reusable";
PrefetchCacheEntryStatus["expired"] = "expired";
PrefetchCacheEntryStatus["stale"] = "stale";
return PrefetchCacheEntryStatus;
}({});
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=router-reducer-types.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import type { ReducerActions, ReducerState, ReadonlyReducerState } from './router-reducer-types';
declare function serverReducer(state: ReadonlyReducerState, _action: ReducerActions): ReducerState;
export declare const reducer: typeof serverReducer;
export {};

View File

@@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "reducer", {
enumerable: true,
get: function() {
return reducer;
}
});
const _routerreducertypes = require("./router-reducer-types");
const _navigatereducer = require("./reducers/navigate-reducer");
const _serverpatchreducer = require("./reducers/server-patch-reducer");
const _restorereducer = require("./reducers/restore-reducer");
const _refreshreducer = require("./reducers/refresh-reducer");
const _prefetchreducer = require("./reducers/prefetch-reducer");
const _hmrrefreshreducer = require("./reducers/hmr-refresh-reducer");
const _serveractionreducer = require("./reducers/server-action-reducer");
/**
* Reducer that handles the app-router state updates.
*/ function clientReducer(state, action) {
switch(action.type){
case _routerreducertypes.ACTION_NAVIGATE:
{
return (0, _navigatereducer.navigateReducer)(state, action);
}
case _routerreducertypes.ACTION_SERVER_PATCH:
{
return (0, _serverpatchreducer.serverPatchReducer)(state, action);
}
case _routerreducertypes.ACTION_RESTORE:
{
return (0, _restorereducer.restoreReducer)(state, action);
}
case _routerreducertypes.ACTION_REFRESH:
{
return (0, _refreshreducer.refreshReducer)(state, action);
}
case _routerreducertypes.ACTION_HMR_REFRESH:
{
return (0, _hmrrefreshreducer.hmrRefreshReducer)(state, action);
}
case _routerreducertypes.ACTION_PREFETCH:
{
return (0, _prefetchreducer.prefetchReducer)(state, action);
}
case _routerreducertypes.ACTION_SERVER_ACTION:
{
return (0, _serveractionreducer.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;
}
const reducer = typeof window === 'undefined' ? serverReducer : clientReducer;
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=router-reducer.js.map

View File

@@ -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_PREFETCH,\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 { prefetchReducer } from './reducers/prefetch-reducer'\nimport { hmrRefreshReducer } from './reducers/hmr-refresh-reducer'\nimport { serverActionReducer } from './reducers/server-action-reducer'\n\n/**\n * Reducer that handles the app-router state updates.\n */\nfunction clientReducer(\n state: ReadonlyReducerState,\n action: ReducerActions\n): ReducerState {\n switch (action.type) {\n case ACTION_NAVIGATE: {\n return navigateReducer(state, action)\n }\n case ACTION_SERVER_PATCH: {\n return serverPatchReducer(state, action)\n }\n case ACTION_RESTORE: {\n return restoreReducer(state, action)\n }\n case ACTION_REFRESH: {\n return refreshReducer(state, action)\n }\n case ACTION_HMR_REFRESH: {\n return hmrRefreshReducer(state, action)\n }\n case ACTION_PREFETCH: {\n return prefetchReducer(state, action)\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":["reducer","clientReducer","state","action","type","ACTION_NAVIGATE","navigateReducer","ACTION_SERVER_PATCH","serverPatchReducer","ACTION_RESTORE","restoreReducer","ACTION_REFRESH","refreshReducer","ACTION_HMR_REFRESH","hmrRefreshReducer","ACTION_PREFETCH","prefetchReducer","ACTION_SERVER_ACTION","serverActionReducer","Error","serverReducer","_action","window"],"mappings":";;;;+BAiEaA;;;eAAAA;;;oCAzDN;iCAMyB;oCACG;gCACJ;gCACA;iCACC;mCACE;qCACE;AAEpC;;CAEC,GACD,SAASC,cACPC,KAA2B,EAC3BC,MAAsB;IAEtB,OAAQA,OAAOC,IAAI;QACjB,KAAKC,mCAAe;YAAE;gBACpB,OAAOC,IAAAA,gCAAe,EAACJ,OAAOC;YAChC;QACA,KAAKI,uCAAmB;YAAE;gBACxB,OAAOC,IAAAA,sCAAkB,EAACN,OAAOC;YACnC;QACA,KAAKM,kCAAc;YAAE;gBACnB,OAAOC,IAAAA,8BAAc,EAACR,OAAOC;YAC/B;QACA,KAAKQ,kCAAc;YAAE;gBACnB,OAAOC,IAAAA,8BAAc,EAACV,OAAOC;YAC/B;QACA,KAAKU,sCAAkB;YAAE;gBACvB,OAAOC,IAAAA,oCAAiB,EAACZ,OAAOC;YAClC;QACA,KAAKY,mCAAe;YAAE;gBACpB,OAAOC,IAAAA,gCAAe,EAACd,OAAOC;YAChC;QACA,KAAKc,wCAAoB;YAAE;gBACzB,OAAOC,IAAAA,wCAAmB,EAAChB,OAAOC;YACpC;QACA,+DAA+D;QAC/D;YACE,MAAM,qBAA2B,CAA3B,IAAIgB,MAAM,mBAAV,qBAAA;uBAAA;4BAAA;8BAAA;YAA0B;IACpC;AACF;AAEA,SAASC,cACPlB,KAA2B,EAC3BmB,OAAuB;IAEvB,OAAOnB;AACT;AAGO,MAAMF,UACX,OAAOsB,WAAW,cAAcF,gBAAgBnB"}

View File

@@ -0,0 +1,18 @@
import type { RequestHeaders } from './fetch-server-response';
/**
* 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 declare const setCacheBustingSearchParam: (url: URL, headers: RequestHeaders) => void;

View File

@@ -0,0 +1,44 @@
'use client';
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "setCacheBustingSearchParam", {
enumerable: true,
get: function() {
return setCacheBustingSearchParam;
}
});
const _hash = require("../../../shared/lib/hash");
const _approuterheaders = require("../app-router-headers");
const setCacheBustingSearchParam = (url, headers)=>{
const uniqueCacheKey = (0, _hash.hexHash)([
headers[_approuterheaders.NEXT_ROUTER_PREFETCH_HEADER] || '0',
headers[_approuterheaders.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] || '0',
headers[_approuterheaders.NEXT_ROUTER_STATE_TREE_HEADER],
headers[_approuterheaders.NEXT_URL]
].join(','));
/**
* 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;
const pairs = rawQuery.split('&').filter(Boolean);
pairs.push(_approuterheaders.NEXT_RSC_UNION_QUERY + "=" + uniqueCacheKey);
url.search = pairs.length ? "?" + pairs.join('&') : '';
};
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=set-cache-busting-search-param.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/client/components/router-reducer/set-cache-busting-search-param.ts"],"sourcesContent":["'use client'\nimport { hexHash } from '../../../shared/lib/hash'\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 = hexHash(\n [\n headers[NEXT_ROUTER_PREFETCH_HEADER] || '0',\n headers[NEXT_ROUTER_SEGMENT_PREFETCH_HEADER] || '0',\n headers[NEXT_ROUTER_STATE_TREE_HEADER],\n headers[NEXT_URL],\n ].join(',')\n )\n\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 const pairs = rawQuery.split('&').filter(Boolean)\n pairs.push(`${NEXT_RSC_UNION_QUERY}=${uniqueCacheKey}`)\n url.search = pairs.length ? `?${pairs.join('&')}` : ''\n}\n"],"names":["setCacheBustingSearchParam","url","headers","uniqueCacheKey","hexHash","NEXT_ROUTER_PREFETCH_HEADER","NEXT_ROUTER_SEGMENT_PREFETCH_HEADER","NEXT_ROUTER_STATE_TREE_HEADER","NEXT_URL","join","existingSearch","search","rawQuery","startsWith","slice","pairs","split","filter","Boolean","push","NEXT_RSC_UNION_QUERY","length"],"mappings":"AAAA;;;;;+BA2BaA;;;eAAAA;;;sBA1BW;kCAOjB;AAmBA,MAAMA,6BAA6B,CACxCC,KACAC;IAEA,MAAMC,iBAAiBC,IAAAA,aAAO,EAC5B;QACEF,OAAO,CAACG,6CAA2B,CAAC,IAAI;QACxCH,OAAO,CAACI,qDAAmC,CAAC,IAAI;QAChDJ,OAAO,CAACK,+CAA6B,CAAC;QACtCL,OAAO,CAACM,0BAAQ,CAAC;KAClB,CAACC,IAAI,CAAC;IAGT;;;;;;;;;;GAUC,GACD,MAAMC,iBAAiBT,IAAIU,MAAM;IACjC,MAAMC,WAAWF,eAAeG,UAAU,CAAC,OACvCH,eAAeI,KAAK,CAAC,KACrBJ;IACJ,MAAMK,QAAQH,SAASI,KAAK,CAAC,KAAKC,MAAM,CAACC;IACzCH,MAAMI,IAAI,CAAC,AAAGC,sCAAoB,GAAC,MAAGjB;IACtCF,IAAIU,MAAM,GAAGI,MAAMM,MAAM,GAAG,AAAC,MAAGN,MAAMN,IAAI,CAAC,OAAS;AACtD"}

View File

@@ -0,0 +1,2 @@
import type { FlightRouterState, FlightDataPath } from '../../../server/app-render/types';
export declare function shouldHardNavigate(flightSegmentPath: FlightDataPath, flightRouterState: FlightRouterState): boolean;

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "shouldHardNavigate", {
enumerable: true,
get: function() {
return shouldHardNavigate;
}
});
const _flightdatahelpers = require("../../flight-data-helpers");
const _matchsegments = require("../match-segments");
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 (!(0, _matchsegments.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((0, _flightdatahelpers.getNextFlightSegmentPath)(flightSegmentPath), parallelRoutes[parallelRouteKey]);
}
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
Object.defineProperty(exports.default, '__esModule', { value: true });
Object.assign(exports.default, exports);
module.exports = exports.default;
}
//# sourceMappingURL=should-hard-navigate.js.map

View File

@@ -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 '../../../server/app-render/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":["shouldHardNavigate","flightSegmentPath","flightRouterState","segment","parallelRoutes","currentSegment","parallelRouteKey","matchSegment","Array","isArray","lastSegment","length","getNextFlightSegmentPath"],"mappings":";;;;+BASgBA;;;eAAAA;;;mCAJyB;+BACZ;AAGtB,SAASA,mBACdC,iBAAiC,EACjCC,iBAAoC;IAEpC,MAAM,CAACC,SAASC,eAAe,GAAGF;IAClC,2CAA2C;IAC3C,MAAM,CAACG,gBAAgBC,iBAAiB,GAAGL;IAK3C,yDAAyD;IACzD,IAAI,CAACM,IAAAA,2BAAY,EAACF,gBAAgBF,UAAU;QAC1C,kGAAkG;QAClG,IAAIK,MAAMC,OAAO,CAACJ,iBAAiB;YACjC,OAAO;QACT;QAEA,sEAAsE;QACtE,OAAO;IACT;IACA,MAAMK,cAAcT,kBAAkBU,MAAM,IAAI;IAEhD,IAAID,aAAa;QACf,OAAO;IACT;IAEA,OAAOV,mBACLY,IAAAA,2CAAwB,EAACX,oBACzBG,cAAc,CAACE,iBAAiB;AAEpC"}