next
Version:
The React Framework
278 lines (277 loc) • 14 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
// eslint-disable-next-line import/no-extraneous-dependencies
import { createFromReadableStream } from 'react-server-dom-webpack/client.edge';
// eslint-disable-next-line import/no-extraneous-dependencies
import { renderToReadableStream } from 'react-server-dom-webpack/server.edge';
import { streamFromBuffer, streamToBuffer } from '../stream-utils/node-web-streams-helper';
import { UNDERSCORE_NOT_FOUND_ROUTE } from '../../api/constants';
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler';
export async function collectSegmentData(fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest) {
// Traverse the router tree and generate a prefetch response for each segment.
// A mutable map to collect the results as we traverse the route tree.
const resultMap = new Map();
// Before we start, warm up the module cache by decoding the page data once.
// Then we can assume that any remaining async tasks that occur the next time
// are due to hanging promises caused by dynamic data access. Note we only
// have to do this once per page, not per individual segment.
//
try {
await createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
serverConsumerManifest
});
await waitAtLeastOneReactRenderTask();
} catch {}
// Generate a stream for the route tree prefetch. While we're walking the
// tree, we'll also spawn additional tasks to generate the segment prefetches.
// The promises for these tasks are pushed to a mutable array that we will
// await once the route tree is fully rendered.
const segmentTasks = [];
const treeStream = await renderToReadableStream(// RootTreePrefetch is not a valid return type for a React component, but
// we need to use a component so that when we decode the original stream
// inside of it, the side effects are transferred to the new stream.
// @ts-expect-error
/*#__PURE__*/ _jsx(PrefetchTreeData, {
fullPageDataBuffer: fullPageDataBuffer,
serverConsumerManifest: serverConsumerManifest,
clientModules: clientModules,
staleTime: staleTime,
segmentTasks: segmentTasks
}), clientModules, {
// Unlike when rendering the segment streams, we do not pass an abort
// controller here. There's nothing dynamic in the prefetch metadata; we
// will always render the result. We do still have to account for hanging
// promises, but we use a different strategy. See PrefetchTreeData.
onError () {
// Ignore any errors. These would have already been reported when
// we created the full page data.
}
});
// Write the route tree to a special `/_tree` segment.
const treeBuffer = await streamToBuffer(treeStream);
resultMap.set('/_tree', treeBuffer);
// Now that we've finished rendering the route tree, all the segment tasks
// should have been spawned. Await them in parallel and write the segment
// prefetches to the result map.
for (const [segmentPath, buffer] of (await Promise.all(segmentTasks))){
resultMap.set(segmentPath, buffer);
}
return resultMap;
}
async function PrefetchTreeData({ fullPageDataBuffer, serverConsumerManifest, clientModules, staleTime, segmentTasks }) {
// We're currently rendering a Flight response for the route tree prefetch.
// Inside this component, decode the Flight stream for the whole page. This is
// a hack to transfer the side effects from the original Flight stream (e.g.
// Float preloads) onto the Flight stream for the tree prefetch.
// TODO: React needs a better way to do this. Needed for Server Actions, too.
const initialRSCPayload = await createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
serverConsumerManifest
});
// FlightDataPath is an unsound type, hence the additional checks.
const flightDataPaths = initialRSCPayload.f;
if (flightDataPaths.length !== 1 && flightDataPaths[0].length !== 3) {
console.error('Internal Next.js error: InitialRSCPayload does not match the expected ' + 'shape for a prerendered page during segment prefetch generation.');
return null;
}
const flightRouterState = flightDataPaths[0][0];
const seedData = flightDataPaths[0][1];
// Compute the route metadata tree by traversing the FlightRouterState. As we
// walk the tree, we will also spawn a task to produce a prefetch response for
// each segment.
const tree = await collectSegmentDataImpl(flightRouterState, seedData, fullPageDataBuffer, clientModules, serverConsumerManifest, '', '', segmentTasks);
// Render the route tree to a special `/_tree` segment.
const treePrefetch = {
tree,
staleTime
};
return treePrefetch;
}
async function collectSegmentDataImpl(route, seedData, fullPageDataBuffer, clientModules, serverConsumerManifest, segmentPathStr, accessToken, segmentTasks) {
// Metadata about the segment. Sent as part of the tree prefetch. Null if
// there are no children.
let slotMetadata = null;
// Access tokens for the child segments. Sent as part of layout's data. Null
// if there are no children.
let childAccessTokens = null;
const children = route[1];
const seedDataChildren = seedData[2];
for(const parallelRouteKey in children){
const childRoute = children[parallelRouteKey];
const childSegment = childRoute[0];
const childSeedData = seedDataChildren[parallelRouteKey];
if (!childSeedData) {
continue;
}
const childSegmentPathStr = segmentPathStr + '/' + encodeChildSegmentAsFilesystemSafePathname(parallelRouteKey, childSegment);
// Create an access token for each child slot.
const childAccessToken = await createSegmentAccessToken(segmentPathStr, parallelRouteKey);
const childTree = await collectSegmentDataImpl(childRoute, seedData, fullPageDataBuffer, clientModules, serverConsumerManifest, childSegmentPathStr, childAccessToken, segmentTasks);
if (slotMetadata === null) {
slotMetadata = {};
}
slotMetadata[parallelRouteKey] = childTree;
if (childAccessTokens === null) {
childAccessTokens = {};
}
childAccessTokens[parallelRouteKey] = childAccessToken;
}
// Spawn a task to write the segment data to a new Flight stream.
segmentTasks.push(// Since we're already in the middle of a render, wait until after the
// current task to escape the current rendering context.
waitAtLeastOneReactRenderTask().then(()=>renderSegmentPrefetch(seedData, segmentPathStr, accessToken, childAccessTokens, clientModules)));
// Metadata about the segment. Sent to the client as part of the
// tree prefetch.
const segment = route[0];
const isRootLayout = route[4];
return {
key: segmentPathStr === '' ? '/' : segmentPathStr,
slots: slotMetadata,
extra: [
segment,
isRootLayout === true
]
};
}
async function renderSegmentPrefetch(seedData, segmentPathStr, accessToken, childAccessTokens, clientModules) {
// Render the segment data to a stream.
// In the future, this is where we can include additional metadata, like the
// stale time and cache tags.
const rsc = seedData[1];
const loading = seedData[3];
const segmentPrefetch = {
rsc,
loading,
slots: childAccessTokens
};
// Since all we're doing is decoding and re-encoding a cached prerender, if
// it takes longer than a microtask, it must because of hanging promises
// caused by dynamic data. Abort the stream at the end of the current task.
const abortController = new AbortController();
waitAtLeastOneReactRenderTask().then(()=>abortController.abort());
const segmentStream = await renderToReadableStream(segmentPrefetch, clientModules, {
signal: abortController.signal,
onError () {
// Ignore any errors. These would have already been reported when
// we created the full page data.
}
});
const segmentBuffer = await streamToBuffer(segmentStream);
// Add the buffer to the result map.
if (segmentPathStr === '') {
return [
'/',
segmentBuffer
];
} else {
// The access token is appended to the end of the segment name. To request
// a segment, the client sends a header like:
//
// Next-Router-Segment-Prefetch: /path/to/segment.accesstoken
//
// The segment path is provided by the tree prefetch, and the access
// token is provided in the parent layout's data.
const fullPath = `${segmentPathStr}.${accessToken}`;
return [
fullPath,
segmentBuffer
];
}
}
// TODO: Consider updating or unifying this encoding logic for segments with
// createRouterCacheKey on the client, perhaps by including it as part of
// the FlightRouterState. Theoretically the client should never have to do its
// own encoding of segment keys; it can pass back whatever the server gave it.
function encodeChildSegmentAsFilesystemSafePathname(parallelRouteKey, segment) {
// Encode a child segment and its corresponding parallel route key to a
// filesystem-safe pathname. The format is internal-only and can be somewhat
// arbitrary as long as there are no collisions, because these will be used
// as filenames during build and in the incremental cache. They will also
// be sent by the client to request the corresponding segment, but they
// do not need to be decodable. The server will merely look for a matching
// file in the cache.
//
// For ease of debugging, the format looks roughly similar to the App Router
// convention for defining routes in the source, but again the exact format is
// not important as long as it's consistent between the client and server and
// meets the above requirements.
//
// TODO: If the segment did not read from params, then we can omit the
// params from the cache key. Need to track this during the prerender somehow.
let safeSegmentValue;
if (typeof segment === 'string') {
safeSegmentValue = encodeParamValue(segment);
} else {
// Parameterized segments.
const [paramName, paramValue, paramType] = segment;
let paramPrefix;
switch(paramType){
case 'c':
case 'ci':
paramPrefix = `[...${paramName}]`;
break;
case 'oc':
paramPrefix = `[[...${paramName}]]`;
break;
case 'd':
case 'di':
paramPrefix = `[${paramName}]`;
break;
default:
throw new Error('Unknown dynamic param type');
}
safeSegmentValue = `${paramPrefix}-${encodeParamValue(paramValue)}`;
}
let result;
if (parallelRouteKey === 'children') {
// Omit the parallel route key for children, since this is the most
// common case. Saves some bytes.
result = `${safeSegmentValue}`;
} else {
result = `@${parallelRouteKey}/${safeSegmentValue}`;
}
return result;
}
// Define a regex pattern to match the most common characters found in a route
// param. It excludes anything that might not be cross-platform filesystem
// compatible, like |. It does not need to be precise because the fallback is to
// just base64url-encode the whole parameter, which is fine; we just don't do it
// by default for compactness, and for easier debugging.
const simpleParamValueRegex = /^[a-zA-Z0-9\-_@]+$/;
function encodeParamValue(segment) {
if (segment === UNDERSCORE_NOT_FOUND_ROUTE) {
// TODO: FlightRouterState encodes Not Found routes as "/_not-found". But
// params typically don't include the leading slash. We should use a
// different encoding to avoid this special case.
return '_not-found';
}
if (simpleParamValueRegex.test(segment)) {
return segment;
}
// If there are any unsafe characters, base64url-encode the entire segment.
// We also add a $ prefix so it doesn't collide with the simple case.
return '$' + Buffer.from(segment, 'utf-8').toString('base64url');
}
async function createSegmentAccessToken(parentSegmentPathStr, parallelRouteKey) {
// Create an access token that the client passes when requesting a segment.
// The token is sent to the client as part of the parent layout's data.
//
// The token is hash of the parent segment path and the parallel route key. A
// subtle detail here is that it does *not* include the value of the segment
// itself — a shared layout must produce the same access tokens for its
// children regardless of their segment values, so that the client only has to
// fetch the layout once.
//
// TODO: Because this only affects prefetches, this doesn't need to be secure.
// It's just for obfuscation. But eventually we will use this technique when
// performing dynamic navigations, to support auth checks in a layout that
// conditionally renders its slots. At that point we'll need to add a salt.
// Encode the inputs as Uint8Array
const encoder = new TextEncoder();
const data = encoder.encode(parentSegmentPathStr + parallelRouteKey);
// Use the Web Crypto API to generate a SHA-256 hash.
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// Convert the ArrayBuffer to a hex string
const hashArray = new Uint8Array(hashBuffer);
const hashHex = Array.from(hashArray).map((byte)=>byte.toString(16).padStart(2, '0')).join('');
return hashHex;
}
//# sourceMappingURL=collect-segment-data.js.map