next
Version:
The React Framework
221 lines (220 loc) • 10.4 kB
JavaScript
import { parseAppSegmentConfig } from './app-segment-config';
import { InvariantError } from '../../../shared/lib/invariant-error';
import { isAppRouteRouteModule, isAppPageRouteModule } from '../../../server/route-modules/checks';
import { isClientReference } from '../../../lib/client-and-server-references';
import { getSegmentParam } from '../../../shared/lib/router/utils/get-segment-param';
import { getLayoutOrPageModule } from '../../../server/lib/app-dir-module';
import { resolveEnsureStaticConfig } from '../../../server/app-render/segment-config/ensure-static';
import { anySegmentHasPartialPrefetchingEnabled } from '../../../server/app-render/instant-validation/instant-config';
import { isPlainObject } from '../../../shared/lib/is-plain-object';
export const PARAM_MATCHING_MODES = [
'not-found',
'blocking',
'fallback',
'dynamic'
];
function validateParamMatchingExport(route, filePath, exportName, value, visibleParamNames) {
if (!isPlainObject(value)) {
const valueType = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
throw new Error(`Invalid value from \`${exportName}\` for "${route}". Expected an object, but received ${valueType}.`);
}
const paramMatching = {};
for (const [paramName, mode] of Object.entries(value)){
if (!visibleParamNames.includes(paramName)) {
throw new Error(`Invalid parameter "${paramName}" in \`${exportName}\` for "${route}". The export in "${filePath}" may only configure parameters defined at or above its segment.`);
}
if (!PARAM_MATCHING_MODES.includes(mode)) {
throw new Error(`Invalid mode for parameter "${paramName}" in \`${exportName}\` for "${route}". Expected "not-found", "blocking", "fallback", or "dynamic", but received ${JSON.stringify(mode)}.`);
}
paramMatching[paramName] = mode;
}
return paramMatching;
}
/**
* Parses the app config and attaches it to the segment.
*/ function attach(segment, userland, route, visibleParamNames) {
// If the userland is not an object, then we can't do anything with it.
if (typeof userland !== 'object' || userland === null) {
return;
}
// Try to parse the application configuration.
const config = parseAppSegmentConfig(userland, route);
// If there was any keys on the config, then attach it to the segment.
if (Object.keys(config).length > 0) {
segment.config = config;
}
if ('generateStaticParams' in userland && typeof userland.generateStaticParams === 'function') {
var _segment_config;
segment.generateStaticParams = userland.generateStaticParams;
// Compiler-injected factory whose error stack is anchored at the user's
// `generateStaticParams` declaration. Used to throw a meaningful error when
// an empty result is detected under Cache Components.
const createEmptyParamsError = userland.__next_create_empty_gsp_error;
if (typeof createEmptyParamsError === 'function') {
segment.createEmptyParamsError = createEmptyParamsError;
}
// Validate that `generateStaticParams` makes sense in this context.
if (((_segment_config = segment.config) == null ? void 0 : _segment_config.runtime) === 'edge') {
throw new Error('Edge runtime is not supported with `generateStaticParams`.');
}
}
const hasStaticParamMatching = 'unstable_paramMatching' in userland;
const hasGeneratedParamMatching = 'unstable_generateParamMatching' in userland;
if (hasStaticParamMatching || hasGeneratedParamMatching) {
if (hasStaticParamMatching && hasGeneratedParamMatching) {
throw new Error(`Route "${route}" cannot export both \`unstable_paramMatching\` and \`unstable_generateParamMatching\`.`);
}
if (hasStaticParamMatching) {
segment.paramMatching = validateParamMatchingExport(route, segment.filePath, 'unstable_paramMatching', userland.unstable_paramMatching, visibleParamNames);
} else {
const generate = userland.unstable_generateParamMatching;
if (typeof generate !== 'function') {
throw new Error(`Route "${route}" must export \`unstable_generateParamMatching\` as a function.`);
}
const { filePath } = segment;
// Retain the module's scope without evaluating user code until static
// path generation has established its work store and cache context.
segment.paramMatching = async ()=>validateParamMatchingExport(route, filePath, 'unstable_generateParamMatching', await generate(), visibleParamNames);
}
}
}
/**
* Walks the loader tree and collects the generate parameters for each segment.
*
* @param routeModule the app page route module
* @returns the segments for the app page route module
*/ async function collectAppPageSegments(routeModule, config) {
// We keep track of unique segments, since with parallel routes, it's possible
// to see the same segment multiple times.
const segments = [];
const segmentTree = [];
const rootLoaderTree = routeModule.userland.loaderTree;
// `ensureStatic` needs to be validated against the tree structure,
// so we cannot compute it easily in `reduceAppConfig` after the tree
// is turned into an array of segments
const routeHasPartialPrefetching = ((config == null ? void 0 : config.partialPrefetching) ?? false) || await anySegmentHasPartialPrefetchingEnabled(rootLoaderTree);
const prefetchConfig = routeHasPartialPrefetching ? 'partial' : 'auto';
const ensureStatic = await resolveEnsureStaticConfig(rootLoaderTree, routeHasPartialPrefetching);
// Queue will store loader trees.
const queue = [
{
loaderTree: rootLoaderTree,
visibleParamNames: [],
parentChildren: segmentTree
}
];
while(queue.length > 0){
const { loaderTree, visibleParamNames, parentChildren } = queue.shift();
const [name, parallelRoutes] = loaderTree;
// Process current node
const { mod: userland, filePath } = await getLayoutOrPageModule(loaderTree);
const isClientComponent = userland && isClientReference(userland);
const param = getSegmentParam(name);
const currentVisibleParamNames = param ? [
...visibleParamNames,
param.paramName
] : visibleParamNames;
const segment = {
name,
paramName: param == null ? void 0 : param.paramName,
paramType: param == null ? void 0 : param.paramType,
filePath,
config: undefined,
paramMatching: undefined,
generateStaticParams: undefined
};
// Only server components can have app segment configurations
if (!isClientComponent) {
attach(segment, userland, routeModule.definition.pathname, currentVisibleParamNames);
if (segment.config) {
segment.config.prefetch = prefetchConfig;
segment.config.ensureStatic = ensureStatic;
}
}
// If this segment doesn't already exist, then add it to the segments array.
// The list of segments is short so we just use a list traversal to check
// for duplicates and spare us needing to maintain the string key.
if (segments.every((s)=>s.name !== segment.name || s.paramName !== segment.paramName || s.paramType !== segment.paramType || s.filePath !== segment.filePath)) {
segments.push(segment);
}
const children = [];
parentChildren.push([
segment,
children
]);
// Add all parallel routes to the queue
for (const parallelRoute of Object.values(parallelRoutes)){
queue.push({
loaderTree: parallelRoute,
visibleParamNames: currentVisibleParamNames,
parentChildren: children
});
}
}
return {
segments,
segmentTree
};
}
/**
* Collects the segments for a given app route module.
*
* @param routeModule the app route module
* @returns the segments for the app route module
*/ async function collectAppRouteSegments(routeModule) {
// The route file may be an async module (top-level await), so the userland
// module must be resolved before its exports can be inspected.
await routeModule.ensureUserland();
// Get the pathname parts, slice off the first element (which is empty).
const parts = routeModule.definition.pathname.split('/').slice(1);
if (parts.length === 0) {
throw new InvariantError('Expected at least one segment');
}
// Generate all the segments.
const segments = parts.map((name)=>{
const param = getSegmentParam(name);
return {
name,
paramName: param == null ? void 0 : param.paramName,
paramType: param == null ? void 0 : param.paramType,
filePath: undefined,
config: undefined,
paramMatching: undefined,
generateStaticParams: undefined
};
});
// We know we have at least one, we verified this above. We should get the
// last segment which represents the root route module.
const segment = segments[segments.length - 1];
segment.filePath = routeModule.definition.filename;
// Extract the segment config from the userland module.
attach(segment, routeModule.userland, routeModule.definition.pathname, []);
let segmentTree = [];
for(let index = segments.length - 1; index >= 0; index--){
segmentTree = [
[
segments[index],
segmentTree
]
];
}
return {
segments,
segmentTree
};
}
/**
* Collects the segments for a given route module.
*
* @param components the loaded components
* @returns the segments for the route module
*/ export function collectSegments(routeModule, config) {
if (isAppRouteRouteModule(routeModule)) {
return collectAppRouteSegments(routeModule);
}
if (isAppPageRouteModule(routeModule)) {
return collectAppPageSegments(routeModule, config);
}
throw new InvariantError('Expected a route module to be one of app route or page');
}
//# sourceMappingURL=app-segments.js.map