UNPKG

next

Version:

The React Framework

566 lines (565 loc) • 25.5 kB
// 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