next
Version:
The React Framework
566 lines (565 loc) • 25.5 kB
JavaScript
// eslint-disable-next-line import/no-extraneous-dependencies
import 'server-only';
import { createDefaultMetadata, createDefaultViewport } from './default-metadata';
import { getSegmentParam } from '../../shared/lib/router/utils/get-segment-param';
import { getComponentTypeModule, getLayoutOrPageModule } from '../../server/lib/app-dir-module';
import { createServerParamsForMetadata } from '../../server/request/params';
import { DEFAULT_SEGMENT_KEY, PAGE_SEGMENT_KEY } from '../../shared/lib/segment';
import { PARALLEL_ROUTE_DEFAULT_PATH } from '../../client/components/builtin/default';
import { PARALLEL_ROUTE_DEFAULT_NULL_PATH } from '../../client/components/builtin/default-null';
import { workAsyncStorage } from '../../server/app-render/work-async-storage.external';
import { InvariantError } from '../../shared/lib/invariant-error';
import * as Log from '../../build/output/log';
import { getUseCacheFunctionInfo } from '../client-and-server-references';
import { createLazyResult } from '../../server/lib/lazy-result';
import { getAccessFallbackErrorTypeByStatus, getAccessFallbackHTTPStatus, isHTTPAccessFallbackError } from '../../client/components/http-access-fallback/http-access-fallback';
import { isRedirectError } from '../../client/components/redirect-error';
import { convertUrlsToStrings, createSelectedMetadata, getDefinedMetadata, getDefinedViewport, isFavicon, mergeMetadata, mergeViewport, postProcessMetadata, resolveStaticMetadata } from './metadata-resolution-primitives';
const METADATA = 1;
const VIEWPORT = 2;
const METADATA_AND_VIEWPORT = 3;
async function collectMetadataAndViewport({ tree, props, route, errorConvention, resolutionTarget }) {
const shouldResolveMetadata = Boolean(resolutionTarget & METADATA);
const shouldResolveViewport = Boolean(resolutionTarget & VIEWPORT);
const moduleResultPromise = errorConvention ? getComponentTypeModule(tree, 'layout').then((mod)=>({
mod,
modType: errorConvention
})) : getLayoutOrPageModule(tree);
const staticFilesMetadata = shouldResolveMetadata ? resolveStaticMetadata(tree[2], props) : Promise.resolve(null);
// Static metadata is resolved eagerly, but its rejection belongs to this
// branch and will be replayed through the branch's metadata outlet.
staticFilesMetadata.catch(()=>null);
const moduleResult = await moduleResultPromise;
const hasStaticFilesMetadata = shouldResolveMetadata && hasStaticMetadataFiles(tree);
if (moduleResult.modType) {
route += `/${moduleResult.modType}`;
}
const metadata = shouldResolveMetadata && moduleResult.mod ? getDefinedMetadata(moduleResult.mod, props, {
route
}) : null;
const viewport = shouldResolveViewport && moduleResult.mod ? getDefinedViewport(moduleResult.mod, props, {
route
}) : null;
let errorLayer = null;
if (errorConvention && tree[2][errorConvention]) {
const errorMod = await getComponentTypeModule(tree, errorConvention);
errorLayer = {
metadata: shouldResolveMetadata && errorMod ? getDefinedMetadata(errorMod, props, {
route
}) : null,
viewport: shouldResolveViewport && errorMod ? getDefinedViewport(errorMod, props, {
route
}) : null,
staticFilesMetadata,
hasStaticFilesMetadata
};
}
return {
metadata,
viewport,
staticFilesMetadata,
hasStaticFilesMetadata,
errorLayer
};
}
function callResolver(resolver) {
let result;
try {
result = resolver();
} catch (error) {
result = Promise.reject(error);
}
if (result instanceof Promise) {
// Generators are eagerly executed, so attach a rejection handler before
// an earlier layer can suspend or fail.
result.catch(()=>null);
}
return result;
}
function getResult(exportForResult) {
if (typeof exportForResult === 'function') {
// If the function is a 'use cache' function that uses the parent data as
// the second argument, we don't want to eagerly execute it during
// metadata/viewport pre-rendering, as the parent data might also be
// computed from another 'use cache' function. To ensure that the hanging
// input abort signal handling works in this case (i.e. the depending
// function waits for the cached input to resolve while encoding its args),
// they must be called sequentially. This can be accomplished by wrapping
// the call in a lazy promise, so that the original function is only called
// when the result is actually awaited.
const useCacheFunctionInfo = getUseCacheFunctionInfo(exportForResult.$$original);
if (useCacheFunctionInfo && !useCacheFunctionInfo.usedArgs[1]) {
return {
resolveParent: null,
result: callResolver(()=>{
// @ts-expect-error We intentionally omit the parent argument, because
// we know this 'use cache' function does not use it.
return exportForResult();
})
};
}
let resolveParent;
const parent = new Promise((resolve)=>{
resolveParent = resolve;
});
return {
resolveParent: resolveParent,
result: useCacheFunctionInfo ? createLazyResult(()=>exportForResult(parent)) : callResolver(()=>exportForResult(parent))
};
}
return {
resolveParent: null,
result: typeof exportForResult === 'object' ? exportForResult : null
};
}
function resolveParentResult(parentResult, resolveParent) {
if (process.env.NODE_ENV === 'development') {
parentResult = require('../../shared/lib/deep-freeze').deepFreeze(structuredClone(parentResult));
}
resolveParent(parentResult);
}
function cloneStaticMetadata(metadata) {
if (!metadata) return null;
return {
...metadata,
icon: metadata.icon ? [
...metadata.icon
] : undefined,
apple: metadata.apple ? [
...metadata.apple
] : undefined,
openGraph: metadata.openGraph ? [
...metadata.openGraph
] : undefined,
twitter: metadata.twitter ? [
...metadata.twitter
] : undefined
};
}
function createMetadataAccumulator() {
return {
metadata: createDefaultMetadata(),
titleTemplates: {
title: null,
twitter: null,
openGraph: null
},
favicon: null,
leafSegmentStaticIcons: {
icon: [],
apple: []
},
buildState: {
warnings: new Set()
}
};
}
function cloneMetadataAccumulator(accumulator) {
return {
metadata: structuredClone(accumulator.metadata),
titleTemplates: {
...accumulator.titleTemplates
},
favicon: accumulator.favicon ? structuredClone(accumulator.favicon) : null,
leafSegmentStaticIcons: {
icon: structuredClone(accumulator.leafSegmentStaticIcons.icon),
apple: structuredClone(accumulator.leafSegmentStaticIcons.apple)
},
buildState: {
warnings: new Set(accumulator.buildState.warnings)
}
};
}
function cloneViewportAccumulator(accumulator) {
return {
viewport: structuredClone(accumulator.viewport)
};
}
async function accumulateMetadataLayer(parent, prerendered, staticMetadata, route, layerIndex, pathname, metadataContext) {
var _staticFilesMetadata_icon;
const accumulator = await parent;
const staticFilesMetadata = cloneStaticMetadata(await staticMetadata);
// Treat favicon as a special case. It should be the first icon in the list.
// layerIndex <= 1 represents the root layout and a page at the root.
if (layerIndex <= 1 && isFavicon(staticFilesMetadata == null ? void 0 : (_staticFilesMetadata_icon = staticFilesMetadata.icon) == null ? void 0 : _staticFilesMetadata_icon[0])) {
var _staticFilesMetadata_icon1;
const icon = staticFilesMetadata == null ? void 0 : (_staticFilesMetadata_icon1 = staticFilesMetadata.icon) == null ? void 0 : _staticFilesMetadata_icon1.shift();
if (layerIndex === 0 && icon) {
accumulator.favicon = convertUrlsToStrings(icon);
}
}
if (prerendered.resolveParent) {
resolveParentResult(accumulator.metadata, prerendered.resolveParent);
}
let metadata;
if (isPromiseLike(prerendered.result)) {
metadata = await prerendered.result;
} else {
metadata = prerendered.result;
}
await mergeMetadata(route, pathname, {
metadata,
resolvedMetadata: accumulator.metadata,
staticFilesMetadata,
titleTemplates: accumulator.titleTemplates,
metadataContext,
buildState: accumulator.buildState,
leafSegmentStaticIcons: accumulator.leafSegmentStaticIcons,
cloneResolvedMetadata: false
});
return accumulator;
}
async function accumulateViewportLayer(parent, prerendered) {
const accumulator = await parent;
if (prerendered.resolveParent) {
resolveParentResult(accumulator.viewport, prerendered.resolveParent);
}
let viewport;
if (isPromiseLike(prerendered.result)) {
viewport = await prerendered.result;
} else {
viewport = prerendered.result;
}
mergeViewport({
resolvedViewport: accumulator.viewport,
viewport,
cloneResolvedViewport: false
});
return accumulator;
}
function prepareMetadataAccumulatorForChild(parent, clone, childIsPage, errorConvention) {
return parent.then((parentAccumulator)=>{
const accumulator = clone ? cloneMetadataAccumulator(parentAccumulator) : parentAccumulator;
// A title template applies to a descendant segment, but not to a page in
// the same segment. Error convention metadata is an additional terminal
// layer, so the leaf layout's template does apply to it.
if (!childIsPage || errorConvention) {
var _accumulator_metadata_title, _accumulator_metadata_openGraph, _accumulator_metadata_twitter;
accumulator.titleTemplates = {
title: ((_accumulator_metadata_title = accumulator.metadata.title) == null ? void 0 : _accumulator_metadata_title.template) || null,
openGraph: ((_accumulator_metadata_openGraph = accumulator.metadata.openGraph) == null ? void 0 : _accumulator_metadata_openGraph.title.template) || null,
twitter: ((_accumulator_metadata_twitter = accumulator.metadata.twitter) == null ? void 0 : _accumulator_metadata_twitter.title.template) || null
};
}
return accumulator;
});
}
function prepareViewportAccumulatorForChild(parent, clone) {
return clone ? parent.then(cloneViewportAccumulator) : parent;
}
function completeMetadataAccumulator(accumulator, metadataContext) {
const { leafSegmentStaticIcons, metadata } = accumulator;
if ((leafSegmentStaticIcons.icon.length > 0 || leafSegmentStaticIcons.apple.length > 0) && !metadata.icons) {
metadata.icons = {
icon: [],
apple: []
};
if (leafSegmentStaticIcons.icon.length > 0) {
metadata.icons.icon.unshift(...convertUrlsToStrings(leafSegmentStaticIcons.icon));
}
if (leafSegmentStaticIcons.apple.length > 0) {
metadata.icons.apple.unshift(...convertUrlsToStrings(leafSegmentStaticIcons.apple));
}
}
return postProcessMetadata(metadata, accumulator.favicon, accumulator.titleTemplates, metadataContext);
}
function getResolutionStatus(reason) {
if (isHTTPAccessFallbackError(reason)) {
return getAccessFallbackErrorTypeByStatus(getAccessFallbackHTTPStatus(reason)) || 'error';
}
if (isRedirectError(reason)) {
return 'redirect';
}
return 'error';
}
function createRejectedOutcome(reason) {
return {
status: getResolutionStatus(reason),
reason
};
}
function createMetadataBranchOutcome(pendingAccumulator, metadataContext) {
const completed = pendingAccumulator.then((accumulator)=>({
value: createSelectedMetadata(completeMetadataAccumulator(accumulator, metadataContext)),
warnings: accumulator.buildState.warnings
}));
return completed.then(({ value, warnings })=>({
status: 'resolved',
value,
warnings
}), createRejectedOutcome);
}
function createViewportBranchOutcome(pendingAccumulator) {
return pendingAccumulator.then((accumulator)=>({
status: 'resolved',
value: accumulator.viewport
}), createRejectedOutcome);
}
function createOutletPromise(metadataOutcome, viewportOutcome) {
let pendingOutcomes = 2;
const outlet = new Promise((resolve, reject)=>{
function settle(outcome) {
if (outcome.status !== 'resolved') {
reject(outcome.reason);
} else if (--pendingOutcomes === 0) {
resolve(null);
}
}
metadataOutcome.then(settle, reject);
viewportOutcome.then(settle, reject);
});
// Outlet promises can reject before React renders the corresponding slot.
// Observe the rejection immediately while preserving it for React to replay.
outlet.catch(()=>null);
return outlet;
}
function appendTreeRoute(parentRoute, segment) {
if (segment === PAGE_SEGMENT_KEY) return parentRoute;
if (parentRoute === null) return segment;
return `${parentRoute}/${segment}`;
}
function isPageTree(tree) {
const { layout, page, defaultPage } = tree[2];
return layout === undefined && (page !== undefined || defaultPage !== undefined && tree[0] === DEFAULT_SEGMENT_KEY);
}
function isBuiltinFallback(tree) {
const { defaultPage } = tree[2];
return (defaultPage == null ? void 0 : defaultPage[1].endsWith(PARALLEL_ROUTE_DEFAULT_PATH)) === true || (defaultPage == null ? void 0 : defaultPage[1].endsWith(PARALLEL_ROUTE_DEFAULT_NULL_PATH)) === true;
}
function hasHeadDefinition(layer) {
if (layer === null) return false;
return layer.metadata !== null || layer.viewport !== null || layer.hasStaticFilesMetadata;
}
function hasStaticMetadataFiles(tree) {
var _metadata_icon, _metadata_apple, _metadata_openGraph, _metadata_twitter;
const metadata = tree[2].metadata;
return Boolean(metadata && ((((_metadata_icon = metadata.icon) == null ? void 0 : _metadata_icon.length) ?? 0) > 0 || (((_metadata_apple = metadata.apple) == null ? void 0 : _metadata_apple.length) ?? 0) > 0 || (((_metadata_openGraph = metadata.openGraph) == null ? void 0 : _metadata_openGraph.length) ?? 0) > 0 || (((_metadata_twitter = metadata.twitter) == null ? void 0 : _metadata_twitter.length) ?? 0) > 0 || metadata.manifest !== undefined));
}
function selectDefaultMetadataBranch(branches, forkDepth) {
if (branches.length === 0) {
throw new InvariantError('Expected at least one metadata branch');
}
let selected = branches[0];
for(let i = 1; i < branches.length; i++){
const candidate = branches[i];
if (selected.branch.isBuiltinFallback !== candidate.branch.isBuiltinFallback) {
if (!candidate.branch.isBuiltinFallback) {
selected = candidate;
}
continue;
}
const selectedIsChildren = selected.key === 'children';
const candidateIsChildren = candidate.key === 'children';
if (selectedIsChildren !== candidateIsChildren) {
const childrenBranch = selectedIsChildren ? selected : candidate;
const namedBranch = selectedIsChildren ? candidate : selected;
const childrenDefinesHead = childrenBranch.branch.definitionDepth > forkDepth;
const namedDefinesHead = namedBranch.branch.definitionDepth > forkDepth;
// Prefer children when it contributes to the head. Otherwise, allow a
// named slot with its own definition to provide the selected metadata.
selected = childrenDefinesHead || !namedDefinesHead ? childrenBranch : namedBranch;
continue;
}
if (candidate.branch.definitionDepth > selected.branch.definitionDepth) {
selected = candidate;
continue;
}
if (candidate.branch.definitionDepth < selected.branch.definitionDepth) {
continue;
}
const selectedDepth = selected.branch.keyPath.length;
const candidateDepth = candidate.branch.keyPath.length;
if (candidateDepth > selectedDepth) {
selected = candidate;
continue;
}
if (candidateDepth === selectedDepth && candidate.key < selected.key) {
selected = candidate;
}
}
return selected.branch;
}
async function walkMetadataTree(context, state) {
const [segment, parallelRoutes, { page }] = state.tree;
const treeRoute = appendTreeRoute(state.treeRoute, segment);
const isPage = page !== undefined;
const depth = state.keyPath.length;
const shouldResolveMetadata = Boolean(context.resolutionTarget & METADATA);
const shouldResolveViewport = Boolean(context.resolutionTarget & VIEWPORT);
let currentParams = state.parentParams;
const segmentParam = getSegmentParam(segment);
if (segmentParam) {
const value = context.interpolatedParams[segmentParam.paramName];
if (value !== null && value !== undefined) {
currentParams = {
...state.parentParams,
[segmentParam.paramName]: value
};
}
}
const optionalCatchAllParamName = (segmentParam == null ? void 0 : segmentParam.paramType) === 'optional-catchall' && (context.interpolatedParams[segmentParam.paramName] === null || context.interpolatedParams[segmentParam.paramName] === undefined) ? segmentParam.paramName : state.parentOptionalCatchAllParamName;
const params = createServerParamsForMetadata(currentParams, optionalCatchAllParamName);
const props = isPage ? {
params,
searchParams: context.searchParams
} : {
params
};
const layer = await collectMetadataAndViewport({
tree: state.tree,
props,
route: treeRoute ?? '',
errorConvention: context.errorConvention,
resolutionTarget: context.resolutionTarget
});
let definitionDepth = hasHeadDefinition(layer) ? depth : state.definitionDepth;
// Invoke each active generator as soon as this layer is discovered. Its
// parent promise is resolved later when the preceding accumulator is ready.
let metadata = state.metadataParent;
if (shouldResolveMetadata) {
const prerenderedMetadata = getResult(layer.metadata);
metadata = accumulateMetadataLayer(metadata, prerenderedMetadata, layer.staticFilesMetadata, context.route, depth, context.pathname, context.metadataContext);
}
let viewport = state.viewportParent;
if (shouldResolveViewport) {
const prerenderedViewport = getResult(layer.viewport);
viewport = accumulateViewportLayer(viewport, prerenderedViewport);
}
const errorLayer = layer.errorLayer || state.errorLayer;
let parallelRouteKeys = Object.keys(parallelRoutes);
if (context.selectedKeyPath !== null) {
const selectedKey = context.selectedKeyPath[depth];
if (parallelRouteKeys.length === 0) {
if (selectedKey !== undefined) {
throw new InvariantError('Expected selected metadata branch to end at a leaf');
}
} else {
if (selectedKey === undefined || !parallelRoutes[selectedKey]) {
throw new InvariantError('Expected selected metadata branch to match loader tree');
}
parallelRouteKeys = [
selectedKey
];
}
}
if (parallelRouteKeys.length === 0) {
if (context.errorConvention) {
if (hasHeadDefinition(errorLayer)) {
definitionDepth = depth + 1;
}
if (shouldResolveMetadata) {
const errorMetadata = getResult((errorLayer == null ? void 0 : errorLayer.metadata) || null);
metadata = accumulateMetadataLayer(metadata, errorMetadata, (errorLayer == null ? void 0 : errorLayer.staticFilesMetadata) || Promise.resolve(null), context.route, depth + 1, context.pathname, context.metadataContext);
}
if (shouldResolveViewport) {
const errorViewport = getResult((errorLayer == null ? void 0 : errorLayer.viewport) || null);
viewport = accumulateViewportLayer(viewport, errorViewport);
}
}
const metadataOutcome = createMetadataBranchOutcome(metadata, context.metadataContext);
const viewportOutcome = createViewportBranchOutcome(viewport);
context.outlets.set(state.tree, createOutletPromise(metadataOutcome, viewportOutcome));
return {
metadata: metadataOutcome,
viewport: viewportOutcome,
keyPath: state.keyPath,
definitionDepth,
isBuiltinFallback: isBuiltinFallback(state.tree)
};
}
const cloneAtFork = parallelRouteKeys.length > 1;
const pendingChildBranches = [];
for (const parallelRouteKey of parallelRouteKeys){
const childTree = parallelRoutes[parallelRouteKey];
const childKeyPath = [
...state.keyPath,
parallelRouteKey
];
const branch = walkMetadataTree(context, {
tree: childTree,
treeRoute,
parentParams: currentParams,
parentOptionalCatchAllParamName: optionalCatchAllParamName,
metadataParent: prepareMetadataAccumulatorForChild(metadata, cloneAtFork && shouldResolveMetadata, isPageTree(childTree), context.errorConvention),
viewportParent: prepareViewportAccumulatorForChild(viewport, cloneAtFork && shouldResolveViewport),
errorLayer,
definitionDepth,
keyPath: childKeyPath
});
// Child walks start eagerly, but are awaited in order below. Observe each
// rejection now so a failure in one branch cannot leave a later sibling's
// rejection unhandled.
branch.catch(()=>null);
pendingChildBranches.push({
key: parallelRouteKey,
branch
});
}
const childBranches = [];
for (const pendingChildBranch of pendingChildBranches){
childBranches.push({
key: pendingChildBranch.key,
branch: await pendingChildBranch.branch
});
}
return selectDefaultMetadataBranch(childBranches, depth);
}
async function resolveMetadataTree(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, selectedKeyPath, resolutionTarget) {
const workStore = workAsyncStorage.getStore();
if (!workStore) {
throw new InvariantError('Expected workStore to be initialized');
}
const outlets = new Map();
const selectedBranch = await walkMetadataTree({
pathname,
searchParams,
errorConvention,
interpolatedParams,
metadataContext,
route: workStore.route,
selectedKeyPath,
resolutionTarget,
outlets
}, {
tree,
treeRoute: null,
parentParams: {},
parentOptionalCatchAllParamName: null,
metadataParent: Promise.resolve(createMetadataAccumulator()),
viewportParent: Promise.resolve({
viewport: createDefaultViewport()
}),
errorLayer: null,
definitionDepth: -1,
keyPath: []
});
const selected = selectedBranch.metadata.then((outcome)=>{
if (outcome.status === 'resolved') {
for (const warning of outcome.warnings){
Log.warn(warning);
}
}
return outcome;
});
return {
selectedKeyPath: selectedBranch.keyPath,
selectedMetadata: selected,
selectedViewport: selectedBranch.viewport,
outlets
};
}
export function resolveMetadataResolution(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext) {
return resolveMetadataTree(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, null, METADATA_AND_VIEWPORT);
}
export async function resolveMetadataForBranch(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, selectedKeyPath) {
const resolution = await resolveMetadataTree(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, selectedKeyPath, METADATA);
return resolution.selectedMetadata;
}
export async function resolveViewportForBranch(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, selectedKeyPath) {
const resolution = await resolveMetadataTree(tree, pathname, searchParams, errorConvention, interpolatedParams, metadataContext, selectedKeyPath, VIEWPORT);
return resolution.selectedViewport;
}
function isPromiseLike(value) {
return typeof value === 'object' && value !== null && typeof value.then === 'function';
}
//# sourceMappingURL=resolve-metadata-parallel.js.map