expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
194 lines • 7.79 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseParameter = exports.getServerManifest = void 0;
const matchers_1 = require("./matchers");
const sortRoutes_1 = require("./sortRoutes");
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) {
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 === 'api') {
key = (0, matchers_1.getContextKey)(route.contextKey).replace(/\/index$/, '') ?? '/';
}
else {
key = (0, matchers_1.getContextKey)(absoluteRoute).replace(/\/index$/, '') ?? '/';
}
return [[key, '/' + absoluteRoute, route]];
}
// Remove duplicates from the runtime manifest which expands array syntax.
const flat = getFlatNodes(route)
.sort(([, , a], [, , b]) => (0, sortRoutes_1.sortRoutes)(b, a))
.reverse();
const apiRoutes = uniqueBy(flat.filter(([, , route]) => route.type === 'api'), ([path]) => path);
const otherRoutes = uniqueBy(flat.filter(([, , route]) => route.type === 'route'), ([path]) => path);
const standardRoutes = otherRoutes.filter(([, , route]) => !isNotFoundRoute(route));
const notFoundRoutes = otherRoutes.filter(([, , route]) => isNotFoundRoute(route));
return {
apiRoutes: getMatchableManifestForPaths(apiRoutes),
htmlRoutes: getMatchableManifestForPaths(standardRoutes),
notFoundRoutes: getMatchableManifestForPaths(notFoundRoutes),
};
}
exports.getServerManifest = getServerManifest;
function getMatchableManifestForPaths(paths) {
return paths.map(([normalizedRoutePath, absoluteRoute, node]) => {
const matcher = getNamedRouteRegex(normalizedRoutePath, absoluteRoute, node.contextKey);
if (node.generated) {
matcher.generated = true;
}
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, matchers_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 };
}
exports.parseParameter = parseParameter;
//# sourceMappingURL=getServerManifest.js.map
;