733 lines
33 KiB
JavaScript
733 lines
33 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getRoutes = getRoutes;
|
|
exports.extrapolateGroups = extrapolateGroups;
|
|
exports.generateDynamic = generateDynamic;
|
|
const matchers_1 = require("./matchers");
|
|
const url_1 = require("./utils/url");
|
|
const validPlatforms = new Set(['android', 'ios', 'native', 'web']);
|
|
/**
|
|
* Given a Metro context module, return an array of nested routes.
|
|
*
|
|
* This is a two step process:
|
|
* 1. Convert the RequireContext keys (file paths) into a directory tree.
|
|
* - This should extrapolate array syntax into multiple routes
|
|
* - Routes are given a specificity score
|
|
* 2. Flatten the directory tree into routes
|
|
* - Routes in directories without _layout files are hoisted to the nearest _layout
|
|
* - The name of the route is relative to the nearest _layout
|
|
* - If multiple routes have the same name, the most specific route is used
|
|
*/
|
|
function getRoutes(contextModule, options) {
|
|
const middleware = getMiddleware(contextModule, options);
|
|
const directoryTree = getDirectoryTree(contextModule, options);
|
|
// If there are no routes
|
|
if (!directoryTree) {
|
|
return null;
|
|
}
|
|
const rootNode = flattenDirectoryTreeToRoutes(directoryTree, options);
|
|
if (middleware) {
|
|
rootNode.middleware = middleware;
|
|
}
|
|
if (!options.ignoreEntryPoints) {
|
|
crawlAndAppendInitialRoutesAndEntryFiles(rootNode, options);
|
|
}
|
|
return rootNode;
|
|
}
|
|
/**
|
|
* Given a RequireContext, return the middleware node if one is found. If more than one middleware file is found, an error is thrown.
|
|
*/
|
|
function getMiddleware(contextModule, options) {
|
|
const allMiddlewareFiles = contextModule.keys().filter((key) => key.includes('+middleware'));
|
|
// Check if middleware is enabled via plugin config
|
|
if (!options.unstable_useServerMiddleware) {
|
|
if (allMiddlewareFiles.length > 0) {
|
|
console.warn('Server middleware is not enabled. Add unstable_useServerMiddleware: true to your `expo-router` plugin config.\n\n' +
|
|
JSON.stringify({
|
|
expo: {
|
|
plugins: [['expo-router', { unstable_useServerMiddleware: true }]],
|
|
},
|
|
}, null, 2));
|
|
}
|
|
return null;
|
|
}
|
|
const isValidMiddleware = (key) => /^\.\/\+middleware\.[tj]sx?$/.test(key);
|
|
const rootMiddlewareFiles = allMiddlewareFiles.filter(isValidMiddleware);
|
|
const nonRootMiddleware = allMiddlewareFiles.filter((file) => !rootMiddlewareFiles.includes(file));
|
|
if (nonRootMiddleware.length > 0) {
|
|
throw new Error(`The middleware file can only be placed at the root level. Remove the following files: ${nonRootMiddleware.join(', ')}`);
|
|
}
|
|
if (rootMiddlewareFiles.length === 0) {
|
|
return null;
|
|
}
|
|
// In development, throw an error if there are multiple root-level middleware files
|
|
if (rootMiddlewareFiles.length > 1) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
throw new Error(`Only one middleware file is allowed. Keep one of the conflicting files: ${rootMiddlewareFiles.map((p) => `"${p}"`).join(' or ')}`);
|
|
}
|
|
}
|
|
const middlewareFilePath = rootMiddlewareFiles[0];
|
|
const middleware = {
|
|
loadRoute() {
|
|
if (options.ignoreRequireErrors) {
|
|
try {
|
|
return contextModule(middlewareFilePath);
|
|
}
|
|
catch {
|
|
return {};
|
|
}
|
|
}
|
|
else {
|
|
return contextModule(middlewareFilePath);
|
|
}
|
|
},
|
|
contextKey: middlewareFilePath,
|
|
};
|
|
if (options.internal_stripLoadRoute) {
|
|
delete middleware.loadRoute;
|
|
}
|
|
return middleware;
|
|
}
|
|
/**
|
|
* Converts the RequireContext keys (file paths) into a directory tree.
|
|
*/
|
|
function getDirectoryTree(contextModule, options) {
|
|
const importMode = options.importMode || process.env.EXPO_ROUTER_IMPORT_MODE;
|
|
const ignoreList = [/^\.\/\+(html|native-intent)\.[tj]sx?$/]; // Ignore the top level ./+html file
|
|
if (options.ignore) {
|
|
ignoreList.push(...options.ignore);
|
|
}
|
|
if (!options.preserveApiRoutes) {
|
|
ignoreList.push(/\+api$/, /\+api\.[tj]sx?$/);
|
|
}
|
|
// Always ignore middleware files in regular route processing
|
|
ignoreList.push(/\+middleware$/, /\+middleware\.[tj]sx?$/);
|
|
const rootDirectory = {
|
|
files: new Map(),
|
|
subdirectories: new Map(),
|
|
};
|
|
let hasRoutes = false;
|
|
let isValid = false;
|
|
const contextKeys = contextModule.keys();
|
|
const redirects = {};
|
|
const rewrites = {};
|
|
let validRedirectDestinations;
|
|
const getValidDestinations = () => {
|
|
// Loop over contexts once and cache the valid destinations
|
|
validRedirectDestinations ??= contextKeys.map((key) => {
|
|
return {
|
|
contextKey: key,
|
|
nameWithoutInvisible: getNameWithoutInvisibleSegmentsFromRedirectPath((0, matchers_1.removeSupportedExtensions)(key)),
|
|
};
|
|
});
|
|
return validRedirectDestinations;
|
|
};
|
|
// If we are keeping redirects as valid routes, then we need to add them to the contextKeys
|
|
// This is useful for generating a sitemap with redirects, or static site generation that includes redirects
|
|
if (options.preserveRedirectAndRewrites) {
|
|
if (options.redirects) {
|
|
for (const redirect of options.redirects) {
|
|
const sourceContextKey = getSourceContextKeyFromRedirectSource(redirect.source);
|
|
const sourceName = getNameFromRedirectPath(redirect.source);
|
|
const isExternalRedirect = (0, url_1.shouldLinkExternally)(redirect.destination);
|
|
const targetDestinationName = isExternalRedirect
|
|
? redirect.destination
|
|
: getNameWithoutInvisibleSegmentsFromRedirectPath(redirect.destination);
|
|
if (ignoreList.some((regex) => regex.test(sourceContextKey))) {
|
|
continue;
|
|
}
|
|
const validDestination = isExternalRedirect
|
|
? undefined
|
|
: getValidDestinations().find((key) => key.nameWithoutInvisible === targetDestinationName);
|
|
const destination = isExternalRedirect
|
|
? targetDestinationName
|
|
: validDestination?.nameWithoutInvisible;
|
|
const destinationContextKey = isExternalRedirect
|
|
? targetDestinationName
|
|
: validDestination?.contextKey;
|
|
if (!destinationContextKey || destination === undefined) {
|
|
/*
|
|
* Only throw the error when we are preserving the api routes
|
|
* When doing a static export, API routes will not exist so the redirect destination may not exist.
|
|
* The desired behavior for this error is to warn the user when running `expo start`, so its ok if
|
|
* `expo export` swallows this error.
|
|
*/
|
|
if (options.preserveApiRoutes) {
|
|
throw new Error(`Redirect destination "${redirect.destination}" does not exist.`);
|
|
}
|
|
continue;
|
|
}
|
|
contextKeys.push(sourceContextKey);
|
|
redirects[sourceName] = {
|
|
source: sourceName,
|
|
destination,
|
|
destinationContextKey,
|
|
permanent: Boolean(redirect.permanent),
|
|
external: isExternalRedirect,
|
|
methods: redirect.methods,
|
|
};
|
|
}
|
|
}
|
|
if (options.rewrites) {
|
|
for (const rewrite of options.rewrites) {
|
|
const sourceContextKey = getSourceContextKeyFromRedirectSource(rewrite.source);
|
|
const sourceName = getNameFromRedirectPath(rewrite.source);
|
|
// We check to see if the context key is already known so that we don't create a rewrite for
|
|
// a route that already exists on disk
|
|
const isSourceContextKeyAlreadyKnown = contextKeys.includes(sourceContextKey);
|
|
const targetDestinationName = isSourceContextKeyAlreadyKnown
|
|
? getNameFromRedirectPath(rewrite.destination)
|
|
: getNameWithoutInvisibleSegmentsFromRedirectPath(rewrite.destination);
|
|
if (ignoreList.some((regex) => regex.test(sourceContextKey))) {
|
|
continue;
|
|
}
|
|
const validDestination = getValidDestinations().find((key) => key.nameWithoutInvisible === targetDestinationName);
|
|
const destination = validDestination?.nameWithoutInvisible;
|
|
const destinationContextKey = validDestination?.contextKey;
|
|
if (!destinationContextKey || destination === undefined) {
|
|
/*
|
|
* Only throw the error when we are preserving the api routes
|
|
* When doing a static export, API routes will not exist so the redirect destination may not exist.
|
|
* The desired behavior for this error is to warn the user when running `expo start`, so its ok if
|
|
* `expo export` swallows this error.
|
|
*/
|
|
if (options.preserveApiRoutes) {
|
|
throw new Error(`Rewrite destination "${rewrite.destination}" does not exist.`);
|
|
}
|
|
continue;
|
|
}
|
|
contextKeys.push(sourceContextKey);
|
|
rewrites[sourceName] = {
|
|
source: sourceName,
|
|
destination,
|
|
destinationContextKey,
|
|
methods: rewrite.methods,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
const processedRedirectsRewrites = new Set();
|
|
for (const filePath of contextKeys) {
|
|
if (ignoreList.some((regex) => regex.test(filePath))) {
|
|
continue;
|
|
}
|
|
isValid = true;
|
|
const meta = getFileMeta(filePath, options, redirects, rewrites);
|
|
// This is a file that should be ignored. e.g maybe it has an invalid platform?
|
|
if (meta.specificity < 0) {
|
|
continue;
|
|
}
|
|
let node = {
|
|
type: meta.isApi ? 'api' : meta.isLayout ? 'layout' : 'route',
|
|
loadRoute() {
|
|
let routeModule;
|
|
if (options.ignoreRequireErrors) {
|
|
try {
|
|
routeModule = contextModule(filePath);
|
|
}
|
|
catch {
|
|
routeModule = {};
|
|
}
|
|
}
|
|
else {
|
|
routeModule = contextModule(filePath);
|
|
}
|
|
if (process.env.NODE_ENV === 'development' && importMode === 'sync') {
|
|
// In development mode, when async routes are disabled, add some extra error handling to improve the developer experience.
|
|
// This can be useful when you accidentally use an async function in a route file for the default export.
|
|
if (routeModule instanceof Promise) {
|
|
throw new Error(`Route "${filePath}" cannot be a promise when async routes is disabled.`);
|
|
}
|
|
const defaultExport = routeModule?.default;
|
|
if (defaultExport instanceof Promise) {
|
|
throw new Error(`The default export from route "${filePath}" is a promise. Ensure the React Component does not use async or promises.`);
|
|
}
|
|
// check if default is an async function without invoking it
|
|
if (defaultExport instanceof Function &&
|
|
// This only works on web because Hermes support async functions so we have to transform them out.
|
|
defaultExport.constructor.name === 'AsyncFunction') {
|
|
throw new Error(`The default export from route "${filePath}" is an async function. Ensure the React Component does not use async or promises.`);
|
|
}
|
|
// Validate loader export in development
|
|
const loaderExport = routeModule?.loader;
|
|
if (loaderExport && typeof loaderExport !== 'function') {
|
|
throw new Error(`Route "${filePath}" exports a loader that is not a function.`);
|
|
}
|
|
}
|
|
return routeModule;
|
|
},
|
|
contextKey: filePath,
|
|
route: '', // This is overwritten during hoisting based upon the _layout
|
|
dynamic: null,
|
|
children: [], // While we are building the directory tree, we don't know the node's children just yet. This is added during hoisting
|
|
};
|
|
if (meta.isRedirect) {
|
|
if (processedRedirectsRewrites.has(meta.route)) {
|
|
continue;
|
|
}
|
|
const redirect = redirects[meta.route];
|
|
node.destinationContextKey = redirect.destinationContextKey;
|
|
node.permanent = redirect.permanent;
|
|
node.generated = true;
|
|
if (node.type === 'route') {
|
|
node = options.getSystemRoute({
|
|
type: 'redirect',
|
|
route: redirect.destination,
|
|
defaults: node,
|
|
redirectConfig: redirect,
|
|
});
|
|
}
|
|
if (redirect.methods) {
|
|
node.methods = redirect.methods;
|
|
}
|
|
node.type = 'redirect';
|
|
processedRedirectsRewrites.add(meta.route);
|
|
}
|
|
if (meta.isRewrite) {
|
|
if (processedRedirectsRewrites.has(meta.route)) {
|
|
continue;
|
|
}
|
|
const rewrite = rewrites[meta.route];
|
|
node.destinationContextKey = rewrite.destinationContextKey;
|
|
node.generated = true;
|
|
if (node.type === 'route') {
|
|
node = options.getSystemRoute({
|
|
type: 'rewrite',
|
|
route: rewrite.destination,
|
|
defaults: node,
|
|
rewriteConfig: rewrite,
|
|
});
|
|
}
|
|
if (rewrite.methods) {
|
|
node.methods = rewrite.methods;
|
|
}
|
|
node.type = 'rewrite';
|
|
processedRedirectsRewrites.add(meta.route);
|
|
}
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// If the user has set the `EXPO_ROUTER_IMPORT_MODE` to `sync` then we should
|
|
// filter the missing routes.
|
|
if (node.type !== 'api' && importMode === 'sync') {
|
|
const routeItem = node.loadRoute();
|
|
// Have a warning for nullish ex
|
|
const route = routeItem?.default;
|
|
if (route == null) {
|
|
// Do not throw an error since a user may just be creating a new route.
|
|
console.warn(`Route "${filePath}" is missing the required default export. Ensure a React component is exported as default.`);
|
|
continue;
|
|
}
|
|
if (['boolean', 'number', 'string'].includes(typeof route)) {
|
|
throw new Error(`The default export from route "${filePath}" is an unsupported type: "${typeof route}". Only React Components are supported as default exports from route files.`);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* A single filepath may be extrapolated into multiple routes if it contains array syntax.
|
|
* Another way to thinking about is that a filepath node is present in multiple leaves of the directory tree.
|
|
*/
|
|
for (const route of extrapolateGroups(meta.route)) {
|
|
// Traverse the directory tree to its leaf node, creating any missing directories along the way
|
|
const subdirectoryParts = route.split('/').slice(0, -1);
|
|
// Start at the root directory and traverse the path to the leaf directory
|
|
let directory = rootDirectory;
|
|
for (const part of subdirectoryParts) {
|
|
let subDirectory = directory.subdirectories.get(part);
|
|
// Create any missing subdirectories
|
|
if (!subDirectory) {
|
|
subDirectory = {
|
|
files: new Map(),
|
|
subdirectories: new Map(),
|
|
};
|
|
directory.subdirectories.set(part, subDirectory);
|
|
}
|
|
directory = subDirectory;
|
|
}
|
|
// Clone the node for this route
|
|
node = { ...node, route };
|
|
if (meta.isLayout) {
|
|
directory.layout ??= [];
|
|
const existing = directory.layout[meta.specificity];
|
|
if (existing) {
|
|
// In production, use the first route found
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
throw new Error(`The layouts "${filePath}" and "${existing.contextKey}" conflict on the route "/${route}". Remove or rename one of these files.`);
|
|
}
|
|
}
|
|
else {
|
|
node = getLayoutNode(node, options);
|
|
directory.layout[meta.specificity] = node;
|
|
}
|
|
}
|
|
else if (meta.isApi) {
|
|
const fileKey = `${route}+api`;
|
|
let nodes = directory.files.get(fileKey);
|
|
if (!nodes) {
|
|
nodes = [];
|
|
directory.files.set(fileKey, nodes);
|
|
}
|
|
// API Routes have no specificity, they are always the first node
|
|
const existing = nodes[0];
|
|
if (existing) {
|
|
// In production, use the first route found
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
throw new Error(`The API route file "${filePath}" and "${existing.contextKey}" conflict on the route "/${route}". Remove or rename one of these files.`);
|
|
}
|
|
}
|
|
else {
|
|
nodes[0] = node;
|
|
}
|
|
}
|
|
else {
|
|
let nodes = directory.files.get(route);
|
|
if (!nodes) {
|
|
nodes = [];
|
|
directory.files.set(route, nodes);
|
|
}
|
|
/**
|
|
* If there is an existing node with the same specificity, then we have a conflict.
|
|
* NOTE(Platform Routes):
|
|
* We cannot check for specificity conflicts here, as we haven't processed all the context keys yet!
|
|
* This will be checked during hoisting, as well as enforcing that all routes have a non-platform route.
|
|
*/
|
|
const existing = nodes[meta.specificity];
|
|
if (existing) {
|
|
// In production, use the first route found
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
throw new Error(`The route files "${filePath}" and "${existing.contextKey}" conflict on the route "/${route}". Remove or rename one of these files.`);
|
|
}
|
|
}
|
|
else {
|
|
hasRoutes ||= true;
|
|
nodes[meta.specificity] = node;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If there are no routes/layouts then we should display the tutorial.
|
|
if (!isValid) {
|
|
return null;
|
|
}
|
|
/**
|
|
* If there are no top-level _layout, add a default _layout
|
|
* While this is a generated route, it will still be generated even if skipGenerated is true.
|
|
*/
|
|
if (!rootDirectory.layout) {
|
|
rootDirectory.layout = [
|
|
options.getSystemRoute({
|
|
type: 'layout',
|
|
route: '',
|
|
}),
|
|
];
|
|
}
|
|
// Only include the sitemap if there are routes.
|
|
if (!options.skipGenerated) {
|
|
if (hasRoutes && options.sitemap !== false) {
|
|
appendSitemapRoute(rootDirectory, options);
|
|
}
|
|
if (options.notFound !== false) {
|
|
appendNotFoundRoute(rootDirectory, options);
|
|
}
|
|
}
|
|
return rootDirectory;
|
|
}
|
|
function getNameFromRedirectPath(path) {
|
|
// Removing only the filesystem extensions, to be able to handle +api, +html
|
|
return ((0, matchers_1.removeFileSystemExtensions)((0, matchers_1.removeFileSystemDots)(path))
|
|
// Remove the leading `/`
|
|
.replace(/^\//, ''));
|
|
}
|
|
function getNameWithoutInvisibleSegmentsFromRedirectPath(path) {
|
|
return (0, matchers_1.stripInvisibleSegmentsFromPath)(getNameFromRedirectPath(path));
|
|
}
|
|
// Creates fake context key for redirects and rewrites
|
|
function getSourceContextKeyFromRedirectSource(source) {
|
|
const name = getNameFromRedirectPath(source);
|
|
const prefix = './';
|
|
const suffix = /\.[tj]sx?$/.test(name) ? '' : '.js'; // Ensure it has a file extension
|
|
return `${prefix}${name}${suffix}`;
|
|
}
|
|
/**
|
|
* Flatten the directory tree into routes, hoisting routes to the nearest _layout.
|
|
*/
|
|
function flattenDirectoryTreeToRoutes(directory, options,
|
|
/* The nearest _layout file in the directory tree */
|
|
layout,
|
|
/* Route names are relative to their layout */
|
|
pathToRemove = '') {
|
|
/**
|
|
* This directory has a _layout file so it becomes the new target for hoisting routes.
|
|
*/
|
|
if (directory.layout) {
|
|
const previousLayout = layout;
|
|
layout = getMostSpecific(directory.layout);
|
|
// Add the new layout as a child of its parent
|
|
if (previousLayout) {
|
|
previousLayout.children.push(layout);
|
|
}
|
|
if (options.internal_stripLoadRoute) {
|
|
delete layout.loadRoute;
|
|
}
|
|
// `route` is the absolute pathname. We need to make this relative to the last _layout
|
|
const newRoute = layout.route.replace(pathToRemove, '');
|
|
pathToRemove = layout.route ? `${layout.route}/` : '';
|
|
// Now update this layout with the new relative route and dynamic conventions
|
|
layout.route = newRoute;
|
|
layout.dynamic = generateDynamic(layout.contextKey.slice(0));
|
|
}
|
|
// This should never occur as there will always be a root layout, but it makes the type system happy
|
|
if (!layout)
|
|
throw new Error('Expo Router Internal Error: No nearest layout');
|
|
for (const routes of directory.files.values()) {
|
|
const routeNode = getMostSpecific(routes);
|
|
// `route` is the absolute pathname. We need to make this relative to the nearest layout
|
|
routeNode.route = routeNode.route.replace(pathToRemove, '');
|
|
routeNode.dynamic = generateDynamic(routeNode.route);
|
|
if (options.internal_stripLoadRoute) {
|
|
delete routeNode.loadRoute;
|
|
}
|
|
layout.children.push(routeNode);
|
|
}
|
|
// Recursively flatten the subdirectories
|
|
for (const child of directory.subdirectories.values()) {
|
|
flattenDirectoryTreeToRoutes(child, options, layout, pathToRemove);
|
|
}
|
|
return layout;
|
|
}
|
|
function getFileMeta(originalKey, options, redirects, rewrites) {
|
|
// Remove the leading `./`
|
|
const key = (0, matchers_1.removeSupportedExtensions)((0, matchers_1.removeFileSystemDots)(originalKey));
|
|
let route = key;
|
|
const parts = (0, matchers_1.removeFileSystemDots)(originalKey).split('/');
|
|
const filename = parts[parts.length - 1];
|
|
const [filenameWithoutExtensions, platformExtension] = (0, matchers_1.removeSupportedExtensions)(filename).split('.');
|
|
const isLayout = filenameWithoutExtensions === '_layout';
|
|
const isApi = originalKey.match(/\+api\.(\w+\.)?[jt]sx?$/);
|
|
if (filenameWithoutExtensions.startsWith('(') && filenameWithoutExtensions.endsWith(')')) {
|
|
throw new Error(`Invalid route ${originalKey}. Routes cannot end with '(group)' syntax`);
|
|
}
|
|
// Nested routes cannot start with the '+' character, except for the '+not-found' route
|
|
if (!isApi && filename.startsWith('+') && filenameWithoutExtensions !== '+not-found') {
|
|
const renamedRoute = [...parts.slice(0, -1), filename.slice(1)].join('/');
|
|
throw new Error(`Invalid route ${originalKey}. Route nodes cannot start with the '+' character. "Rename it to ${renamedRoute}"`);
|
|
}
|
|
let specificity = 0;
|
|
const hasPlatformExtension = validPlatforms.has(platformExtension);
|
|
const usePlatformRoutes = options.platformRoutes ?? true;
|
|
if (hasPlatformExtension) {
|
|
if (!usePlatformRoutes) {
|
|
// If the user has disabled platform routes, then we should ignore this file
|
|
specificity = -1;
|
|
}
|
|
else if (!options.platform) {
|
|
// If we don't have a platform, then we should ignore this file
|
|
// This used by typed routes, sitemap, etc
|
|
specificity = -1;
|
|
}
|
|
else if (platformExtension === options.platform) {
|
|
// If the platform extension is the same as the options.platform, then it is the most specific
|
|
specificity = 2;
|
|
}
|
|
else if (platformExtension === 'native' && options.platform !== 'web') {
|
|
// `native` is allow but isn't as specific as the platform
|
|
specificity = 1;
|
|
}
|
|
else if (platformExtension !== options.platform) {
|
|
// Somehow we have a platform extension that doesn't match the options.platform and it isn't native
|
|
// This is an invalid file and we will ignore it
|
|
specificity = -1;
|
|
}
|
|
if (isApi && specificity !== 0) {
|
|
throw new Error(`API routes cannot have platform extensions. Remove '.${platformExtension}' from '${originalKey}'`);
|
|
}
|
|
route = route.replace(new RegExp(`.${platformExtension}$`), '');
|
|
}
|
|
return {
|
|
route,
|
|
specificity,
|
|
isLayout,
|
|
isApi,
|
|
isRedirect: key in redirects,
|
|
isRewrite: key in rewrites,
|
|
};
|
|
}
|
|
/**
|
|
* Generates a set of strings which have the router array syntax extrapolated.
|
|
*
|
|
* /(a,b)/(c,d)/e.tsx => new Set(['a/c/e.tsx', 'a/d/e.tsx', 'b/c/e.tsx', 'b/d/e.tsx'])
|
|
*/
|
|
function extrapolateGroups(key, keys = new Set()) {
|
|
const match = (0, matchers_1.matchArrayGroupName)(key);
|
|
if (!match) {
|
|
keys.add(key);
|
|
return keys;
|
|
}
|
|
const groups = match.split(',');
|
|
const groupsSet = new Set(groups);
|
|
if (groupsSet.size !== groups.length) {
|
|
throw new Error(`Array syntax cannot contain duplicate group name "${groups}" in "${key}".`);
|
|
}
|
|
if (groups.length === 1) {
|
|
keys.add(key);
|
|
return keys;
|
|
}
|
|
for (const group of groups) {
|
|
extrapolateGroups(key.replace(match, group.trim()), keys);
|
|
}
|
|
return keys;
|
|
}
|
|
function generateDynamic(path) {
|
|
const dynamic = path
|
|
.split('/')
|
|
.map((part) => {
|
|
if (part === '+not-found') {
|
|
return {
|
|
name: '+not-found',
|
|
deep: true,
|
|
notFound: true,
|
|
};
|
|
}
|
|
return (0, matchers_1.matchDynamicName)(part) ?? null;
|
|
})
|
|
.filter((part) => !!part);
|
|
return dynamic.length === 0 ? null : dynamic;
|
|
}
|
|
function appendSitemapRoute(directory, options) {
|
|
if (!directory.files.has('_sitemap') && options.getSystemRoute) {
|
|
directory.files.set('_sitemap', [
|
|
options.getSystemRoute({
|
|
type: 'route',
|
|
route: '_sitemap',
|
|
}),
|
|
]);
|
|
}
|
|
}
|
|
function appendNotFoundRoute(directory, options) {
|
|
if (!directory.files.has('+not-found') && options.getSystemRoute) {
|
|
directory.files.set('+not-found', [
|
|
options.getSystemRoute({
|
|
type: 'route',
|
|
route: '+not-found',
|
|
}),
|
|
]);
|
|
}
|
|
}
|
|
function getLayoutNode(node, options) {
|
|
/**
|
|
* A file called `(a,b)/(c)/_layout.tsx` will generate two _layout routes: `(a)/(c)/_layout` and `(b)/(c)/_layout`.
|
|
* Each of these layouts will have a different anchor based upon the first group name.
|
|
*/
|
|
// We may strip loadRoute during testing
|
|
const groupName = (0, matchers_1.matchLastGroupName)(node.route);
|
|
const childMatchingGroup = node.children.find((child) => {
|
|
return child.route.replace(/\/index$/, '') === groupName;
|
|
});
|
|
let anchor = childMatchingGroup?.route;
|
|
const loaded = node.loadRoute();
|
|
if (loaded?.unstable_settings) {
|
|
try {
|
|
// Allow unstable_settings={ initialRouteName: '...' } to override the default initial route name.
|
|
anchor =
|
|
loaded.unstable_settings.anchor ?? loaded.unstable_settings.initialRouteName ?? anchor;
|
|
}
|
|
catch (error) {
|
|
if (error instanceof Error) {
|
|
if (!error.message.match(/You cannot dot into a client module/)) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
if (groupName) {
|
|
// Allow unstable_settings={ 'custom': { initialRouteName: '...' } } to override the less specific initial route name.
|
|
const groupSpecificInitialRouteName = loaded.unstable_settings?.[groupName]?.anchor ??
|
|
loaded.unstable_settings?.[groupName]?.initialRouteName;
|
|
anchor = groupSpecificInitialRouteName ?? anchor;
|
|
}
|
|
}
|
|
return {
|
|
...node,
|
|
route: node.route.replace(/\/?_layout$/, ''),
|
|
children: [], // Each layout should have its own children
|
|
initialRouteName: anchor,
|
|
};
|
|
}
|
|
function crawlAndAppendInitialRoutesAndEntryFiles(node, options, entryPoints = []) {
|
|
if (node.type === 'route') {
|
|
node.entryPoints = [...new Set([...entryPoints, node.contextKey])];
|
|
}
|
|
else if (node.type === 'redirect') {
|
|
node.entryPoints = [...new Set([...entryPoints, node.destinationContextKey])];
|
|
}
|
|
else if (node.type === 'layout') {
|
|
if (!node.children) {
|
|
throw new Error(`Layout "${node.contextKey}" does not contain any child routes`);
|
|
}
|
|
// Every node below this layout will have it as an entryPoint
|
|
entryPoints = [...entryPoints, node.contextKey];
|
|
/**
|
|
* Calculate the initialRouteNode
|
|
*
|
|
* A file called `(a,b)/(c)/_layout.tsx` will generate two _layout routes: `(a)/(c)/_layout` and `(b)/(c)/_layout`.
|
|
* Each of these layouts will have a different anchor based upon the first group.
|
|
*/
|
|
const groupName = (0, matchers_1.matchGroupName)(node.route);
|
|
const childMatchingGroup = node.children.find((child) => {
|
|
return child.route.replace(/\/index$/, '') === groupName;
|
|
});
|
|
let anchor = childMatchingGroup?.route;
|
|
// We may strip loadRoute during testing
|
|
if (!options.internal_stripLoadRoute) {
|
|
const loaded = node.loadRoute();
|
|
if (loaded?.unstable_settings) {
|
|
try {
|
|
// Allow unstable_settings={ initialRouteName: '...' } to override the default initial route name.
|
|
anchor =
|
|
loaded.unstable_settings.anchor ?? loaded.unstable_settings.initialRouteName ?? anchor;
|
|
}
|
|
catch (error) {
|
|
if (error instanceof Error) {
|
|
if (!error.message.match(/You cannot dot into a client module/)) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
if (groupName) {
|
|
// Allow unstable_settings={ 'custom': { initialRouteName: '...' } } to override the less specific initial route name.
|
|
const groupSpecificInitialRouteName = loaded.unstable_settings?.[groupName]?.anchor ??
|
|
loaded.unstable_settings?.[groupName]?.initialRouteName;
|
|
anchor = groupSpecificInitialRouteName ?? anchor;
|
|
}
|
|
}
|
|
}
|
|
if (anchor) {
|
|
const anchorRoute = node.children.find((child) => child.route === anchor);
|
|
if (!anchorRoute) {
|
|
const validAnchorRoutes = node.children
|
|
.filter((child) => !child.generated)
|
|
.map((child) => `'${child.route}'`)
|
|
.join(', ');
|
|
if (groupName) {
|
|
throw new Error(`Layout ${node.contextKey} has invalid anchor '${anchor}' for group '(${groupName})'. Valid options are: ${validAnchorRoutes}`);
|
|
}
|
|
else {
|
|
throw new Error(`Layout ${node.contextKey} has invalid anchor '${anchor}'. Valid options are: ${validAnchorRoutes}`);
|
|
}
|
|
}
|
|
// Navigators can add initialsRoutes into the history, so they need to be to be included in the entryPoints
|
|
node.initialRouteName = anchor;
|
|
entryPoints.push(anchorRoute.contextKey);
|
|
}
|
|
for (const child of node.children) {
|
|
crawlAndAppendInitialRoutesAndEntryFiles(child, options, entryPoints);
|
|
}
|
|
}
|
|
}
|
|
function getMostSpecific(routes) {
|
|
const route = routes[routes.length - 1];
|
|
if (!routes[0]) {
|
|
throw new Error(`The file ${route.contextKey} does not have a fallback sibling file without a platform extension.`);
|
|
}
|
|
// This works even tho routes is holey array (e.g it might have index 0 and 2 but not 1)
|
|
// `.length` includes the holes in its count
|
|
return routes[routes.length - 1];
|
|
}
|
|
//# sourceMappingURL=getRoutesCore.js.map
|