1"use strict"; 2var __importDefault = (this && this.__importDefault) || function (mod) { 3 return (mod && mod.__esModule) ? mod : { "default": mod }; 4}; 5Object.defineProperty(exports, "__esModule", { value: true }); 6exports.getUserDefinedDeepDynamicRoute = exports.getExactRoutesAsync = exports.getExactRoutes = exports.getRoutesAsync = exports.getRoutes = exports.assertDuplicateRoutes = exports.generateDynamic = exports.generateDynamicFromSegment = exports.getRecursiveTree = void 0; 7const import_mode_1 = __importDefault(require("./import-mode")); 8const matchers_1 = require("./matchers"); 9/** Convert a flat map of file nodes into a nested tree of files. */ 10function getRecursiveTree(files) { 11 const tree = { 12 name: '', 13 children: [], 14 parents: [], 15 node: null, 16 }; 17 for (const file of files) { 18 // ['(tab)', 'settings', '[...another]'] 19 const parts = file.normalizedName.split('/'); 20 let currentNode = tree; 21 for (let i = 0; i < parts.length; i++) { 22 const part = parts[i]; 23 if (i === parts.length - 1 && part === '_layout') { 24 if (currentNode.node) { 25 const overwritten = currentNode.node.contextKey; 26 throw new Error(`Higher priority Layout Route "${file.contextKey}" overriding redundant Layout Route "${overwritten}". Remove the Layout Route "${overwritten}" to fix this.`); 27 } 28 continue; 29 } 30 const existing = currentNode.children.find((item) => item.name === part); 31 if (existing) { 32 currentNode = existing; 33 } 34 else { 35 const newNode = { 36 name: part, 37 children: [], 38 parents: [...currentNode.parents, currentNode.name], 39 node: null, 40 }; 41 currentNode.children.push(newNode); 42 currentNode = newNode; 43 } 44 } 45 currentNode.node = file; 46 } 47 if (process.env.NODE_ENV !== 'production') { 48 assertDeprecatedFormat(tree); 49 } 50 return tree; 51} 52exports.getRecursiveTree = getRecursiveTree; 53function assertDeprecatedFormat(tree) { 54 for (const child of tree.children) { 55 if (child.node && child.children.length && !child.node.normalizedName.endsWith('_layout')) { 56 const ext = child.node.contextKey.split('.').pop(); 57 throw new Error(`Using deprecated Layout Route format: Move \`./app/${child.node.normalizedName}.${ext}\` to \`./app/${child.node.normalizedName}/_layout.${ext}\``); 58 } 59 assertDeprecatedFormat(child); 60 } 61} 62function getTreeNodesAsRouteNodes(nodes) { 63 return nodes 64 .map((node) => treeNodeToRouteNode(node)) 65 .flat() 66 .filter(Boolean); 67} 68function generateDynamicFromSegment(name) { 69 const deepDynamicName = (0, matchers_1.matchDeepDynamicRouteName)(name); 70 const dynamicName = deepDynamicName ?? (0, matchers_1.matchDynamicName)(name); 71 return dynamicName ? { name: dynamicName, deep: !!deepDynamicName } : null; 72} 73exports.generateDynamicFromSegment = generateDynamicFromSegment; 74function generateDynamic(name) { 75 const description = name 76 .split('/') 77 .map((segment) => generateDynamicFromSegment(segment)) 78 .filter(Boolean); 79 return description.length === 0 ? null : description; 80} 81exports.generateDynamic = generateDynamic; 82function collapseRouteSegments(route) { 83 return (0, matchers_1.stripGroupSegmentsFromPath)(route.replace(/\/index$/, '')); 84} 85/** 86 * Given a route node and a name representing the group name, 87 * find the nearest child matching the name. 88 * 89 * Doesn't support slashes in the name. 90 * Routes like `explore/(something)/index` will be matched against `explore`. 91 * 92 */ 93function getDefaultInitialRoute(node, name) { 94 return node.children.find((node) => collapseRouteSegments(node.route) === name); 95} 96function applyDefaultInitialRouteName(node) { 97 const groupName = (0, matchers_1.matchGroupName)(node.route); 98 if (!node.children?.length) { 99 return node; 100 } 101 // Guess at the initial route based on the group name. 102 // TODO(EvanBacon): Perhaps we should attempt to warn when the group doesn't match any child routes. 103 let initialRouteName = groupName ? getDefaultInitialRoute(node, groupName)?.route : undefined; 104 const loaded = node.loadRoute(); 105 if (loaded?.unstable_settings) { 106 // Allow unstable_settings={ initialRouteName: '...' } to override the default initial route name. 107 initialRouteName = loaded.unstable_settings.initialRouteName ?? initialRouteName; 108 if (groupName) { 109 // Allow unstable_settings={ 'custom': { initialRouteName: '...' } } to override the less specific initial route name. 110 const groupSpecificInitialRouteName = loaded.unstable_settings?.[groupName]?.initialRouteName; 111 initialRouteName = groupSpecificInitialRouteName ?? initialRouteName; 112 } 113 } 114 return { 115 ...node, 116 initialRouteName, 117 }; 118} 119function folderNodeToRouteNode({ name, children }) { 120 // Empty folder, skip it. 121 if (!children.length) { 122 return null; 123 } 124 // When there's a directory, but no layout route file (with valid export), the child routes won't be grouped. 125 // This pushes all children into the nearest layout route. 126 return getTreeNodesAsRouteNodes(children.map((child) => { 127 return { 128 ...child, 129 name: [name, child.name].filter(Boolean).join('/'), 130 }; 131 })); 132} 133function fileNodeToRouteNode(tree) { 134 const { name, node, children } = tree; 135 if (!node) 136 throw new Error('node must be defined'); 137 const dynamic = generateDynamic(name); 138 const clones = extrapolateGroupRoutes(name, node.contextKey); 139 clones.delete(name); 140 const output = { 141 loadRoute: node.loadRoute, 142 route: name, 143 contextKey: node.contextKey, 144 children: getTreeNodesAsRouteNodes(children), 145 dynamic, 146 }; 147 if (clones.size) { 148 return [...clones].map((clone) => applyDefaultInitialRouteName({ 149 ...output, 150 contextKey: node.contextKey.replace(output.route, clone), 151 route: clone, 152 })); 153 } 154 return [ 155 applyDefaultInitialRouteName({ 156 loadRoute: node.loadRoute, 157 route: name, 158 contextKey: node.contextKey, 159 children: getTreeNodesAsRouteNodes(children), 160 dynamic, 161 }), 162 ]; 163} 164function extrapolateGroupRoutes(route, contextKey, routes = new Set()) { 165 const match = (0, matchers_1.matchGroupName)(route); 166 if (!match) { 167 routes.add(route); 168 return routes; 169 } 170 const groups = match?.split(','); 171 const groupsSet = new Set(groups); 172 if (groupsSet.size !== groups.length) { 173 throw new Error(`Array syntax cannot contain duplicate group name "${groups}" in "${contextKey}".`); 174 } 175 if (groups.length === 1) { 176 routes.add(route); 177 return routes; 178 } 179 for (const group of groups) { 180 extrapolateGroupRoutes(route.replace(match, group.trim()), contextKey, routes); 181 } 182 return routes; 183} 184function treeNodeToRouteNode(tree) { 185 if (tree.node) { 186 return fileNodeToRouteNode(tree); 187 } 188 return folderNodeToRouteNode(tree); 189} 190function contextModuleToFileNodes(contextModule, options = {}, files = contextModule.keys()) { 191 const nodes = files.map((key) => { 192 // In development, check if the file exports a default component 193 // this helps keep things snappy when creating files. In production we load all screens lazily. 194 try { 195 if (process.env.NODE_ENV === 'development') { 196 // If the user has set the `EXPO_ROUTER_IMPORT_MODE` to `sync` then we should 197 // filter the missing routes. 198 if (import_mode_1.default === 'sync') { 199 const isApi = key.match(/\+api\.[jt]sx?$/); 200 if (!isApi && !contextModule(key)?.default) { 201 return null; 202 } 203 } 204 } 205 const node = { 206 loadRoute() { 207 if (options.ignoreRequireErrors) { 208 try { 209 return contextModule(key); 210 } 211 catch { 212 return {}; 213 } 214 } 215 else { 216 return contextModule(key); 217 } 218 }, 219 normalizedName: (0, matchers_1.getNameFromFilePath)(key), 220 contextKey: key, 221 }; 222 return node; 223 } 224 catch (error) { 225 // Probably this won't stop metro from freaking out but it's worth a try. 226 console.warn('Error loading route "' + key + '"', error); 227 } 228 return null; 229 }); 230 return nodes.filter(Boolean); 231} 232function hasCustomRootLayoutNode(routes) { 233 if (routes.length !== 1) { 234 return false; 235 } 236 // This could either be the root _layout or an app with a single file. 237 const route = routes[0]; 238 if (route.route === '' && route.contextKey.match(/^\.\/_layout\.([jt]sx?)$/)) { 239 return true; 240 } 241 return false; 242} 243function treeNodesToRootRoute(treeNode) { 244 const routes = treeNodeToRouteNode(treeNode); 245 return withOptionalRootLayout(routes); 246} 247function processKeys(files, options) { 248 const { ignore } = options; 249 return files.filter((file) => { 250 return !ignore?.some((pattern) => pattern.test(file)); 251 }); 252} 253/** 254 * Asserts if the require.context has files that share the same name but have different extensions. Exposed for testing. 255 * @private 256 */ 257function assertDuplicateRoutes(filenames) { 258 if (process.env.NODE_ENV === 'production') { 259 return; 260 } 261 const duplicates = filenames 262 .map((filename) => (0, matchers_1.removeSupportedExtensions)(filename)) 263 .reduce((acc, filename) => { 264 acc[filename] = acc[filename] ? acc[filename] + 1 : 1; 265 return acc; 266 }, {}); 267 Object.entries(duplicates).forEach(([filename, count]) => { 268 if (count > 1) { 269 throw new Error(`Multiple files match the route name "${filename}".`); 270 } 271 }); 272} 273exports.assertDuplicateRoutes = assertDuplicateRoutes; 274/** Given a Metro context module, return an array of nested routes. */ 275function getRoutes(contextModule, options) { 276 const route = getExactRoutes(contextModule, options); 277 // If there is no route, return an empty route. 278 if (!route) { 279 return null; 280 } 281 appendSitemapRoute(route); 282 // Auto add not found route if it doesn't exist 283 appendUnmatchedRoute(route); 284 return route; 285} 286exports.getRoutes = getRoutes; 287async function getRoutesAsync(contextModule, options) { 288 const route = await getExactRoutesAsync(contextModule, options); 289 if (!route) { 290 return null; 291 } 292 appendSitemapRoute(route); 293 // Auto add not found route if it doesn't exist 294 appendUnmatchedRoute(route); 295 return route; 296} 297exports.getRoutesAsync = getRoutesAsync; 298function getIgnoreList(options) { 299 const ignore = [/^\.\/\+html\.[tj]sx?$/, ...(options?.ignore ?? [])]; 300 if (options?.preserveApiRoutes !== true) { 301 ignore.push(/\+api\.[tj]sx?$/); 302 } 303 return ignore; 304} 305/** Get routes without unmatched or sitemap. */ 306function getExactRoutes(contextModule, options) { 307 const treeNodes = contextModuleToTree(contextModule, options); 308 const route = treeNodesToRootRoute(treeNodes); 309 return route || null; 310} 311exports.getExactRoutes = getExactRoutes; 312function contextModuleToTree(contextModule, options) { 313 const allowed = processKeys(contextModule.keys(), { 314 ...options, 315 ignore: getIgnoreList(options), 316 }); 317 assertDuplicateRoutes(allowed); 318 const files = contextModuleToFileNodes(contextModule, options, allowed); 319 return getRecursiveTree(files); 320} 321async function getExactRoutesAsync(contextModule, options) { 322 const treeNodes = contextModuleToTree(contextModule, options); 323 const route = treeNodesToRootRoute(treeNodes); 324 return route || null; 325} 326exports.getExactRoutesAsync = getExactRoutesAsync; 327function appendSitemapRoute(routes) { 328 if (!routes.children.length || 329 // Allow overriding the sitemap route 330 routes.children.some((route) => route.route === '_sitemap')) { 331 return routes; 332 } 333 routes.children.push({ 334 loadRoute() { 335 const { Sitemap, getNavOptions } = require('./views/Sitemap'); 336 return { default: Sitemap, getNavOptions }; 337 }, 338 route: '_sitemap', 339 contextKey: './_sitemap.tsx', 340 generated: true, 341 internal: true, 342 dynamic: null, 343 children: [], 344 }); 345 return routes; 346} 347function appendUnmatchedRoute(routes) { 348 // Auto add not found route if it doesn't exist 349 const userDefinedDynamicRoute = getUserDefinedDeepDynamicRoute(routes); 350 if (!userDefinedDynamicRoute) { 351 routes.children.push({ 352 loadRoute() { 353 return { default: require('./views/Unmatched').Unmatched }; 354 }, 355 route: '[...404]', 356 contextKey: './[...404].tsx', 357 dynamic: [{ name: '404', deep: true }], 358 children: [], 359 generated: true, 360 internal: true, 361 }); 362 } 363 return routes; 364} 365/** 366 * Exposed for testing. 367 * @returns a top-level deep dynamic route if it exists, otherwise null. 368 */ 369function getUserDefinedDeepDynamicRoute(routes) { 370 // Auto add not found route if it doesn't exist 371 for (const route of routes.children ?? []) { 372 if (route.generated) 373 continue; 374 const opaqueRoute = (0, matchers_1.stripInvisibleSegmentsFromPath)(route.route); 375 const isDeepDynamic = (0, matchers_1.matchDeepDynamicRouteName)(opaqueRoute); 376 if (isDeepDynamic) { 377 return route; 378 } 379 // Recurse through group routes 380 if ((0, matchers_1.matchGroupName)(route.route)) { 381 const child = getUserDefinedDeepDynamicRoute(route); 382 if (child) { 383 return child; 384 } 385 } 386 } 387 return null; 388} 389exports.getUserDefinedDeepDynamicRoute = getUserDefinedDeepDynamicRoute; 390function withOptionalRootLayout(routes) { 391 if (!routes?.length) { 392 return null; 393 } 394 if (hasCustomRootLayoutNode(routes)) { 395 return routes[0]; 396 } 397 return { 398 loadRoute: () => ({ 399 default: require('./views/Navigator') 400 .DefaultNavigator, 401 }), 402 // Generate a fake file name for the directory 403 contextKey: './_layout.tsx', 404 route: '', 405 generated: true, 406 dynamic: null, 407 children: routes, 408 }; 409} 410//# sourceMappingURL=getRoutes.js.map