next
Version:
The React Framework
317 lines (316 loc) • 15.3 kB
JavaScript
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path';
import { getPathMatch } from '../shared/lib/router/utils/path-match';
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex';
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher';
import { matchHas, prepareDestination } from '../shared/lib/router/utils/prepare-destination';
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash';
import { normalizeRscURL } from '../shared/lib/router/utils/app-paths';
import { NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER, NEXT_CACHE_REVALIDATED_TAGS_HEADER, NEXT_INTERCEPTION_MARKER_PREFIX, NEXT_QUERY_PARAM_PREFIX } from '../lib/constants';
import { normalizeNextQueryParam } from './web/utils';
import { decodeQueryPathParameter } from './lib/decode-query-path-parameter';
import { parseReqUrl } from '../lib/url';
import { formatUrl } from '../shared/lib/router/utils/format-url';
import { parseAndValidateFlightRouterState } from './app-render/parse-and-validate-flight-router-state';
import { isInterceptionRouteRewrite } from '../lib/generate-interception-routes-rewrites';
import { NEXT_ROUTER_STATE_TREE_HEADER } from '../client/components/app-router-headers';
import { getSelectedParams } from '../client/components/router-reducer/compute-changed-path';
function filterInternalQuery(query, paramKeys) {
// this is used to pass query information in rewrites
// but should not be exposed in final query
delete query['nextInternalLocale'];
for(const key in query){
const isNextQueryPrefix = key !== NEXT_QUERY_PARAM_PREFIX && key.startsWith(NEXT_QUERY_PARAM_PREFIX);
const isNextInterceptionMarkerPrefix = key !== NEXT_INTERCEPTION_MARKER_PREFIX && key.startsWith(NEXT_INTERCEPTION_MARKER_PREFIX);
if (isNextQueryPrefix || isNextInterceptionMarkerPrefix || paramKeys.includes(key)) {
delete query[key];
}
}
}
export function normalizeCdnUrl(req, paramKeys) {
// make sure to normalize req.url from CDNs to strip dynamic and rewrite
// params from the query which are added during routing
const _parsedUrl = parseReqUrl(req.url);
// we can't normalize if we can't parse
if (!_parsedUrl) {
return req.url;
}
delete _parsedUrl.search;
filterInternalQuery(_parsedUrl.query, paramKeys);
req.url = formatUrl(_parsedUrl);
}
export function interpolateDynamicPath(pathname, params, defaultRouteRegex) {
if (!defaultRouteRegex) return pathname;
for (const param of Object.keys(defaultRouteRegex.groups)){
const { optional, repeat } = defaultRouteRegex.groups[param];
let builtParam = `[${repeat ? '...' : ''}${param}]`;
if (optional) {
builtParam = `[${builtParam}]`;
}
let paramValue;
const value = params[param];
if (Array.isArray(value)) {
paramValue = value.map((v)=>v && encodeURIComponent(v)).join('/');
} else if (value) {
paramValue = encodeURIComponent(value);
} else {
paramValue = '';
}
if (paramValue || optional) {
pathname = pathname.replaceAll(builtParam, paramValue);
}
}
return pathname;
}
export function normalizeDynamicRouteParams(query, defaultRouteRegex, defaultRouteMatches, ignoreMissingOptional) {
let hasValidParams = true;
let params = {};
for (const key of Object.keys(defaultRouteRegex.groups)){
let value = query[key];
if (typeof value === 'string') {
value = normalizeRscURL(value);
} else if (Array.isArray(value)) {
value = value.map(normalizeRscURL);
}
// if the value matches the default value we can't rely
// on the parsed params, this is used to signal if we need
// to parse x-now-route-matches or not
const defaultValue = defaultRouteMatches[key];
const isOptional = defaultRouteRegex.groups[key].optional;
const isDefaultValue = Array.isArray(defaultValue) ? defaultValue.some((defaultVal)=>{
return Array.isArray(value) ? value.some((val)=>val.includes(defaultVal)) : value == null ? void 0 : value.includes(defaultVal);
}) : value == null ? void 0 : value.includes(defaultValue);
if (isDefaultValue || typeof value === 'undefined' && !(isOptional && ignoreMissingOptional)) {
return {
params: {},
hasValidParams: false
};
}
// non-provided optional values should be undefined so normalize
// them to undefined
if (isOptional && (!value || Array.isArray(value) && value.length === 1 && // fallback optional catch-all SSG pages have
// [[...paramName]] for the root path on Vercel
(value[0] === 'index' || value[0] === `[[...${key}]]`))) {
value = undefined;
delete query[key];
}
// query values from the proxy aren't already split into arrays
// so make sure to normalize catch-all values
if (value && typeof value === 'string' && defaultRouteRegex.groups[key].repeat) {
value = value.split('/');
}
if (value) {
params[key] = value;
}
}
return {
params,
hasValidParams
};
}
export function getServerUtils({ page, i18n, basePath, rewrites, pageIsDynamic, trailingSlash, caseSensitive }) {
let defaultRouteRegex;
let dynamicRouteMatcher;
let defaultRouteMatches;
if (pageIsDynamic) {
defaultRouteRegex = getNamedRouteRegex(page, {
prefixRouteKeys: false
});
dynamicRouteMatcher = getRouteMatcher(defaultRouteRegex);
defaultRouteMatches = dynamicRouteMatcher(page);
}
function handleRewrites(req, parsedUrl) {
const rewriteParams = {};
let fsPathname = parsedUrl.pathname;
const matchesPage = ()=>{
const fsPathnameNoSlash = removeTrailingSlash(fsPathname || '');
return fsPathnameNoSlash === removeTrailingSlash(page) || (dynamicRouteMatcher == null ? void 0 : dynamicRouteMatcher(fsPathnameNoSlash));
};
const checkRewrite = (rewrite)=>{
const matcher = getPathMatch(rewrite.source + (trailingSlash ? '(/)?' : ''), {
removeUnnamedParams: true,
strict: true,
sensitive: !!caseSensitive
});
if (!parsedUrl.pathname) return false;
let params = matcher(parsedUrl.pathname);
if ((rewrite.has || rewrite.missing) && params) {
const hasParams = matchHas(req, parsedUrl.query, rewrite.has, rewrite.missing);
if (hasParams) {
Object.assign(params, hasParams);
} else {
params = false;
}
}
if (params) {
try {
// An interception rewrite might reference a dynamic param for a route the user
// is currently on, which wouldn't be extractable from the matched route params.
// This attempts to extract the dynamic params from the provided router state.
if (isInterceptionRouteRewrite(rewrite)) {
const stateHeader = req.headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()];
if (stateHeader) {
params = {
...getSelectedParams(parseAndValidateFlightRouterState(stateHeader)),
...params
};
}
}
} catch (err) {
// this is a no-op -- we couldn't extract dynamic params from the provided router state,
// so we'll just use the params from the route matcher
}
const { parsedDestination, destQuery } = prepareDestination({
appendParamsToQuery: true,
destination: rewrite.destination,
params: params,
query: parsedUrl.query
});
// if the rewrite destination is external break rewrite chain
if (parsedDestination.protocol) {
return true;
}
Object.assign(rewriteParams, destQuery, params);
Object.assign(parsedUrl.query, parsedDestination.query);
delete parsedDestination.query;
// for each property in parsedUrl.query, if the value is parametrized (eg :foo), look up the value
// in rewriteParams and replace the parametrized value with the actual value
// this is used when the rewrite destination does not contain the original source param
// and so the value is still parametrized and needs to be replaced with the actual rewrite param
Object.entries(parsedUrl.query).forEach(([key, value])=>{
if (value && typeof value === 'string' && value.startsWith(':')) {
const paramName = value.slice(1);
const actualValue = rewriteParams[paramName];
if (actualValue) {
parsedUrl.query[key] = actualValue;
}
}
});
Object.assign(parsedUrl, parsedDestination);
fsPathname = parsedUrl.pathname;
if (!fsPathname) return false;
if (basePath) {
fsPathname = fsPathname.replace(new RegExp(`^${basePath}`), '') || '/';
}
if (i18n) {
const result = normalizeLocalePath(fsPathname, i18n.locales);
fsPathname = result.pathname;
parsedUrl.query.nextInternalLocale = result.detectedLocale || params.nextInternalLocale;
}
if (fsPathname === page) {
return true;
}
if (pageIsDynamic && dynamicRouteMatcher) {
const dynamicParams = dynamicRouteMatcher(fsPathname);
if (dynamicParams) {
parsedUrl.query = {
...parsedUrl.query,
...dynamicParams
};
return true;
}
}
}
return false;
};
for (const rewrite of rewrites.beforeFiles || []){
checkRewrite(rewrite);
}
if (fsPathname !== page) {
let finished = false;
for (const rewrite of rewrites.afterFiles || []){
finished = checkRewrite(rewrite);
if (finished) break;
}
if (!finished && !matchesPage()) {
for (const rewrite of rewrites.fallback || []){
finished = checkRewrite(rewrite);
if (finished) break;
}
}
}
return rewriteParams;
}
function getParamsFromRouteMatches(routeMatchesHeader) {
// If we don't have a default route regex, we can't get params from route
// matches
if (!defaultRouteRegex) return null;
const { groups, routeKeys } = defaultRouteRegex;
const matcher = getRouteMatcher({
re: {
// Simulate a RegExp match from the \`req.url\` input
exec: (str)=>{
// Normalize all the prefixed query params.
const obj = Object.fromEntries(new URLSearchParams(str));
for (const [key, value] of Object.entries(obj)){
const normalizedKey = normalizeNextQueryParam(key);
if (!normalizedKey) continue;
obj[normalizedKey] = value;
delete obj[key];
}
// Use all the named route keys.
const result = {};
for (const keyName of Object.keys(routeKeys)){
const paramName = routeKeys[keyName];
// If this param name is not a valid parameter name, then skip it.
if (!paramName) continue;
const group = groups[paramName];
const value = obj[keyName];
// When we're missing a required param, we can't match the route.
if (!group.optional && !value) return null;
result[group.pos] = value;
}
return result;
}
},
groups
});
const routeMatches = matcher(routeMatchesHeader);
if (!routeMatches) return null;
return routeMatches;
}
function normalizeQueryParams(query, routeParamKeys) {
// this is used to pass query information in rewrites
// but should not be exposed in final query
delete query['nextInternalLocale'];
for (const [key, value] of Object.entries(query)){
const normalizedKey = normalizeNextQueryParam(key);
if (!normalizedKey) continue;
// Remove the prefixed key from the query params because we want
// to consume it for the dynamic route matcher.
delete query[key];
routeParamKeys.add(normalizedKey);
if (typeof value === 'undefined') continue;
query[normalizedKey] = Array.isArray(value) ? value.map((v)=>decodeQueryPathParameter(v)) : decodeQueryPathParameter(value);
}
}
return {
handleRewrites,
defaultRouteRegex,
dynamicRouteMatcher,
defaultRouteMatches,
normalizeQueryParams,
getParamsFromRouteMatches,
/**
* Normalize dynamic route params.
*
* @param query - The query params to normalize.
* @param ignoreMissingOptional - Whether to ignore missing optional params.
* @returns The normalized params and whether they are valid.
*/ normalizeDynamicRouteParams: (query, ignoreMissingOptional)=>{
if (!defaultRouteRegex || !defaultRouteMatches) {
return {
params: {},
hasValidParams: false
};
}
return normalizeDynamicRouteParams(query, defaultRouteRegex, defaultRouteMatches, ignoreMissingOptional);
},
normalizeCdnUrl: (req, paramKeys)=>normalizeCdnUrl(req, paramKeys),
interpolateDynamicPath: (pathname, params)=>interpolateDynamicPath(pathname, params, defaultRouteRegex),
filterInternalQuery: (query, paramKeys)=>filterInternalQuery(query, paramKeys)
};
}
export function getPreviouslyRevalidatedTags(headers, previewModeId) {
return typeof headers[NEXT_CACHE_REVALIDATED_TAGS_HEADER] === 'string' && headers[NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER] === previewModeId ? headers[NEXT_CACHE_REVALIDATED_TAGS_HEADER].split(',') : [];
}
//# sourceMappingURL=server-utils.js.map