252 lines
9.9 KiB
JavaScript
252 lines
9.9 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getServerManifest = getServerManifest;
|
|
exports.parseParameter = parseParameter;
|
|
/**
|
|
* Copyright © 2023 650 Industries.
|
|
* Copyright © 2023 Vercel, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* Based on https://github.com/vercel/next.js/blob/1df2686bc9964f1a86c444701fa5cbf178669833/packages/next/src/shared/lib/router/utils/route-regex.ts
|
|
*/
|
|
const routing_1 = require("expo-router/internal/routing");
|
|
const utils_1 = require("expo-router/internal/utils");
|
|
function isNotFoundRoute(route) {
|
|
return route.dynamic && route.dynamic[route.dynamic.length - 1].notFound;
|
|
}
|
|
function uniqueBy(arr, key) {
|
|
const seen = new Set();
|
|
return arr.filter((item) => {
|
|
const id = key(item);
|
|
if (seen.has(id)) {
|
|
return false;
|
|
}
|
|
seen.add(id);
|
|
return true;
|
|
});
|
|
}
|
|
// Given a nested route tree, return a flattened array of all routes that can be matched.
|
|
function getServerManifest(route, options) {
|
|
function getFlatNodes(route, parentRoute = '') {
|
|
// Use a recreated route instead of contextKey because we duplicate nodes to support array syntax.
|
|
const absoluteRoute = [parentRoute, route.route].filter(Boolean).join('/');
|
|
if (route.children.length) {
|
|
return route.children.map((child) => getFlatNodes(child, absoluteRoute)).flat();
|
|
}
|
|
// API Routes are handled differently to HTML routes because they have no nested behavior.
|
|
// An HTML route can be different based on parent segments due to layout routes, therefore multiple
|
|
// copies should be rendered. However, an API route is always the same regardless of parent segments.
|
|
let key;
|
|
if (route.type.includes('api')) {
|
|
key = getNormalizedContextKey(route.contextKey);
|
|
}
|
|
else {
|
|
key = getNormalizedContextKey(absoluteRoute);
|
|
}
|
|
return [
|
|
{
|
|
normalizedContextKey: key,
|
|
absoluteRoutePath: '/' + absoluteRoute,
|
|
route,
|
|
},
|
|
];
|
|
}
|
|
// Remove duplicates from the runtime manifest which expands array syntax.
|
|
const flat = getFlatNodes(route)
|
|
.sort(({ route: a }, { route: b }) => (0, routing_1.sortRoutes)(b, a))
|
|
.reverse();
|
|
const apiRoutes = uniqueBy(flat.filter(({ route }) => route.type === 'api'), ({ normalizedContextKey }) => normalizedContextKey);
|
|
const otherRoutes = uniqueBy(flat.filter(({ route }) => route.type === 'route' ||
|
|
(route.type === 'rewrite' && (route.methods === undefined || route.methods.includes('GET')))), ({ normalizedContextKey }) => normalizedContextKey);
|
|
const redirects = uniqueBy(flat.filter(({ route }) => route.type === 'redirect'), ({ normalizedContextKey }) => normalizedContextKey)
|
|
.map((redirect) => {
|
|
// TODO(@hassankhan): ENG-16577
|
|
// For external redirects, use `destinationContextKey` as the destination URL
|
|
if ((0, utils_1.shouldLinkExternally)(redirect.route.destinationContextKey)) {
|
|
redirect.absoluteRoutePath = redirect.route.destinationContextKey;
|
|
}
|
|
else {
|
|
redirect.absoluteRoutePath =
|
|
flat.find(({ route }) => route.contextKey === redirect.route.destinationContextKey)
|
|
?.normalizedContextKey ?? '/';
|
|
}
|
|
return redirect;
|
|
})
|
|
.reverse();
|
|
const rewrites = uniqueBy(flat.filter(({ route }) => route.type === 'rewrite'), ({ normalizedContextKey }) => normalizedContextKey)
|
|
.map((rewrite) => {
|
|
rewrite.absoluteRoutePath =
|
|
flat.find(({ route }) => route.contextKey === rewrite.route.destinationContextKey)
|
|
?.normalizedContextKey ?? '/';
|
|
return rewrite;
|
|
})
|
|
.reverse();
|
|
const standardRoutes = otherRoutes.filter(({ route }) => !isNotFoundRoute(route));
|
|
const notFoundRoutes = otherRoutes.filter(({ route }) => isNotFoundRoute(route));
|
|
const manifest = {
|
|
apiRoutes: getMatchableManifestForPaths(apiRoutes),
|
|
htmlRoutes: getMatchableManifestForPaths(standardRoutes),
|
|
notFoundRoutes: getMatchableManifestForPaths(notFoundRoutes),
|
|
redirects: getMatchableManifestForPaths(redirects),
|
|
rewrites: getMatchableManifestForPaths(rewrites),
|
|
};
|
|
if (route.middleware) {
|
|
manifest.middleware = {
|
|
file: route.middleware.contextKey,
|
|
};
|
|
}
|
|
if (options?.headers) {
|
|
manifest.headers = options.headers;
|
|
}
|
|
return manifest;
|
|
}
|
|
function getMatchableManifestForPaths(paths) {
|
|
return paths.map(({ normalizedContextKey, absoluteRoutePath, route }) => {
|
|
const matcher = getNamedRouteRegex(normalizedContextKey, absoluteRoutePath, route.contextKey);
|
|
if (route.generated) {
|
|
matcher.generated = true;
|
|
}
|
|
if (route.permanent) {
|
|
matcher.permanent = true;
|
|
}
|
|
if (route.methods) {
|
|
matcher.methods = route.methods;
|
|
}
|
|
return matcher;
|
|
});
|
|
}
|
|
function getNamedRouteRegex(normalizedRoute, page, file) {
|
|
const result = getNamedParametrizedRoute(normalizedRoute);
|
|
return {
|
|
file,
|
|
page,
|
|
namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,
|
|
routeKeys: result.routeKeys,
|
|
};
|
|
}
|
|
/**
|
|
* Builds a function to generate a minimal routeKey using only a-z and minimal
|
|
* number of characters.
|
|
*/
|
|
function buildGetSafeRouteKey() {
|
|
let currentCharCode = 96; // Starting one before 'a' to make the increment logic simpler
|
|
let currentLength = 1;
|
|
return () => {
|
|
let result = '';
|
|
let incrementNext = true;
|
|
// Iterate from right to left to build the key
|
|
for (let i = 0; i < currentLength; i++) {
|
|
if (incrementNext) {
|
|
currentCharCode++;
|
|
if (currentCharCode > 122) {
|
|
currentCharCode = 97; // Reset to 'a'
|
|
incrementNext = true; // Continue to increment the next character
|
|
}
|
|
else {
|
|
incrementNext = false;
|
|
}
|
|
}
|
|
result = String.fromCharCode(currentCharCode) + result;
|
|
}
|
|
// If all characters are 'z', increase the length of the key
|
|
if (incrementNext) {
|
|
currentLength++;
|
|
currentCharCode = 96; // This will make the next key start with 'a'
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
function removeTrailingSlash(route) {
|
|
return route.replace(/\/$/, '') || '/';
|
|
}
|
|
function getNamedParametrizedRoute(route) {
|
|
const segments = removeTrailingSlash(route).slice(1).split('/');
|
|
const getSafeRouteKey = buildGetSafeRouteKey();
|
|
const routeKeys = {};
|
|
return {
|
|
namedParameterizedRoute: segments
|
|
.map((segment, index) => {
|
|
if (segment === '+not-found' && index === segments.length - 1) {
|
|
segment = '[...not-found]';
|
|
}
|
|
if (/^\[.*\]$/.test(segment)) {
|
|
const { name, optional, repeat } = parseParameter(segment);
|
|
// replace any non-word characters since they can break
|
|
// the named regex
|
|
let cleanedKey = name.replace(/\W/g, '');
|
|
let invalidKey = false;
|
|
// check if the key is still invalid and fallback to using a known
|
|
// safe key
|
|
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
|
|
invalidKey = true;
|
|
}
|
|
if (!isNaN(parseInt(cleanedKey.slice(0, 1), 10))) {
|
|
invalidKey = true;
|
|
}
|
|
// Prevent duplicates after sanitizing the key
|
|
if (cleanedKey in routeKeys) {
|
|
invalidKey = true;
|
|
}
|
|
if (invalidKey) {
|
|
cleanedKey = getSafeRouteKey();
|
|
}
|
|
routeKeys[cleanedKey] = name;
|
|
return repeat
|
|
? optional
|
|
? `(?:/(?<${cleanedKey}>.+?))?`
|
|
: `/(?<${cleanedKey}>.+?)`
|
|
: `/(?<${cleanedKey}>[^/]+?)`;
|
|
}
|
|
else if (/^\(.*\)$/.test(segment)) {
|
|
const groupName = (0, routing_1.matchGroupName)(segment)
|
|
.split(',')
|
|
.map((group) => group.trim())
|
|
.filter(Boolean);
|
|
if (groupName.length > 1) {
|
|
const optionalSegment = `\\((?:${groupName.map(escapeStringRegexp).join('|')})\\)`;
|
|
// Make section optional
|
|
return `(?:/${optionalSegment})?`;
|
|
}
|
|
else {
|
|
// Use simpler regex for single groups
|
|
return `(?:/${escapeStringRegexp(segment)})?`;
|
|
}
|
|
}
|
|
else {
|
|
return `/${escapeStringRegexp(segment)}`;
|
|
}
|
|
})
|
|
.join(''),
|
|
routeKeys,
|
|
};
|
|
}
|
|
// regexp is based on https://github.com/sindresorhus/escape-string-regexp
|
|
const reHasRegExp = /[|\\{}()[\]^$+*?.-]/;
|
|
const reReplaceRegExp = /[|\\{}()[\]^$+*?.-]/g;
|
|
function escapeStringRegexp(str) {
|
|
// see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23
|
|
if (reHasRegExp.test(str)) {
|
|
return str.replace(reReplaceRegExp, '\\$&');
|
|
}
|
|
return str;
|
|
}
|
|
function parseParameter(param) {
|
|
let repeat = false;
|
|
let optional = false;
|
|
let name = param;
|
|
if (/^\[.*\]$/.test(name)) {
|
|
optional = true;
|
|
name = name.slice(1, -1);
|
|
}
|
|
if (/^\.\.\./.test(name)) {
|
|
repeat = true;
|
|
name = name.slice(3);
|
|
}
|
|
return { name, repeat, optional };
|
|
}
|
|
function getNormalizedContextKey(contextKey) {
|
|
return (0, routing_1.getContextKey)(contextKey).replace(/\/index$/, '') ?? '/';
|
|
}
|
|
//# sourceMappingURL=getServerManifest.js.map
|