1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3    if (k2 === undefined) k2 = k;
4    var desc = Object.getOwnPropertyDescriptor(m, k);
5    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6      desc = { enumerable: true, get: function() { return m[k]; } };
7    }
8    Object.defineProperty(o, k2, desc);
9}) : (function(o, m, k, k2) {
10    if (k2 === undefined) k2 = k;
11    o[k2] = m[k];
12}));
13var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14    Object.defineProperty(o, "default", { enumerable: true, value: v });
15}) : function(o, v) {
16    o["default"] = v;
17});
18var __importStar = (this && this.__importStar) || function (mod) {
19    if (mod && mod.__esModule) return mod;
20    var result = {};
21    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22    __setModuleDefault(result, mod);
23    return result;
24};
25var __importDefault = (this && this.__importDefault) || function (mod) {
26    return (mod && mod.__esModule) ? mod : { "default": mod };
27};
28Object.defineProperty(exports, "__esModule", { value: true });
29exports.stripBasePath = exports.getMatchableRouteConfigs = exports.getUrlWithReactNavigationConcessions = void 0;
30const escape_string_regexp_1 = __importDefault(require("escape-string-regexp"));
31const expo_constants_1 = __importDefault(require("expo-constants"));
32const queryString = __importStar(require("query-string"));
33const url_parse_1 = __importDefault(require("url-parse"));
34const findFocusedRoute_1 = require("./findFocusedRoute");
35const validatePathConfig_1 = __importDefault(require("./validatePathConfig"));
36const matchers_1 = require("../matchers");
37function getUrlWithReactNavigationConcessions(path, basePath = expo_constants_1.default.expoConfig?.experiments?.basePath) {
38    const parsed = new url_parse_1.default(path, 'https://acme.com');
39    const pathname = parsed.pathname;
40    // Make sure there is a trailing slash
41    return {
42        // The slashes are at the end, not the beginning
43        nonstandardPathname: stripBasePath(pathname, basePath).replace(/^\/+/g, '').replace(/\/+$/g, '') + '/',
44        // React Navigation doesn't support hashes, so here
45        inputPathnameWithoutHash: stripBasePath(path, basePath).replace(/#.*$/, ''),
46    };
47}
48exports.getUrlWithReactNavigationConcessions = getUrlWithReactNavigationConcessions;
49/**
50 * Utility to parse a path string to initial state object accepted by the container.
51 * This is useful for deep linking when we need to handle the incoming URL.
52 *
53 * @example
54 * ```js
55 * getStateFromPath(
56 *   '/chat/jane/42',
57 *   {
58 *     screens: {
59 *       Chat: {
60 *         path: 'chat/:author/:id',
61 *         parse: { id: Number }
62 *       }
63 *     }
64 *   }
65 * )
66 * ```
67 * @param path Path string to parse and convert, e.g. /foo/bar?count=42.
68 * @param options Extra options to fine-tune how to parse the path.
69 */
70function getStateFromPath(path, options) {
71    const { initialRoutes, configs } = getMatchableRouteConfigs(options);
72    return getStateFromPathWithConfigs(path, configs, initialRoutes);
73}
74exports.default = getStateFromPath;
75function getMatchableRouteConfigs(options) {
76    if (options) {
77        (0, validatePathConfig_1.default)(options);
78    }
79    const screens = options?.screens;
80    // Expo Router disallows usage without a linking config.
81    if (!screens) {
82        throw Error("You must pass a 'screens' object to 'getStateFromPath' to generate a path.");
83    }
84    // This will be mutated...
85    const initialRoutes = [];
86    if (options?.initialRouteName) {
87        initialRoutes.push({
88            initialRouteName: options.initialRouteName,
89            parentScreens: [],
90        });
91    }
92    // Create a normalized configs array which will be easier to use.
93    const converted = Object.keys(screens)
94        .map((key) => createNormalizedConfigs(key, screens, [], initialRoutes))
95        .flat();
96    const resolvedInitialPatterns = initialRoutes.map((route) => joinPaths(...route.parentScreens, route.initialRouteName));
97    const convertedWithInitial = converted.map((config) => ({
98        ...config,
99        // TODO(EvanBacon): Probably a safer way to do this
100        // Mark initial routes to give them potential priority over other routes that match.
101        isInitial: resolvedInitialPatterns.includes(config.routeNames.join('/')),
102    }));
103    // Sort in order of resolution. This is extremely important for the algorithm to work.
104    const configs = convertedWithInitial.sort(sortConfigs);
105    // Assert any duplicates before we start parsing.
106    assertConfigDuplicates(configs);
107    return { configs, initialRoutes };
108}
109exports.getMatchableRouteConfigs = getMatchableRouteConfigs;
110function assertConfigDuplicates(configs) {
111    // Check for duplicate patterns in the config
112    configs.reduce((acc, config) => {
113        // NOTE(EvanBacon): Uses the regex pattern as key to detect duplicate slugs.
114        const indexedKey = config.regex?.toString() ?? config.pattern;
115        const alpha = acc[indexedKey];
116        // NOTE(EvanBacon): Skips checking nodes that have children.
117        if (alpha && !alpha.hasChildren && !config.hasChildren) {
118            const a = alpha.routeNames;
119            const b = config.routeNames;
120            // It's not a problem if the path string omitted from a inner most screen
121            // For example, it's ok if a path resolves to `A > B > C` or `A > B`
122            const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it);
123            if (!intersects) {
124                // NOTE(EvanBacon): Adds more context to the error message since we know about the
125                // file-based routing.
126                const last = config.pattern.split('/').pop();
127                const routeType = last?.startsWith(':')
128                    ? 'dynamic route'
129                    : last?.startsWith('*')
130                        ? 'dynamic-rest route'
131                        : 'route';
132                throw new Error(`The ${routeType} pattern '${config.pattern || '/'}' resolves to both '${alpha.userReadableName}' and '${config.userReadableName}'. Patterns must be unique and cannot resolve to more than one route.`);
133            }
134        }
135        return Object.assign(acc, {
136            [indexedKey]: config,
137        });
138    }, {});
139}
140function sortConfigs(a, b) {
141    // Sort config so that:
142    // - the most exhaustive ones are always at the beginning
143    // - patterns with wildcard are always at the end
144    // If 2 patterns are same, move the one with less route names up
145    // This is an error state, so it's only useful for consistent error messages
146    if (a.pattern === b.pattern) {
147        return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
148    }
149    // If one of the patterns starts with the other, it's more exhaustive
150    // So move it up
151    if (a.pattern.startsWith(b.pattern) &&
152        // NOTE(EvanBacon): This is a hack to make sure that `*` is always at the end
153        b.screen !== 'index') {
154        return -1;
155    }
156    if (b.pattern.startsWith(a.pattern) && a.screen !== 'index') {
157        return 1;
158    }
159    // NOTE(EvanBacon): Here we append `index` if the screen was `index` so the length is the same
160    // as a slug or wildcard when nested more than one level deep.
161    // This is so we can compare the length of the pattern, e.g. `foo/*` > `foo` vs `*` < ``.
162    const aParts = a.pattern
163        .split('/')
164        // Strip out group names to ensure they don't affect the priority.
165        .filter((part) => (0, matchers_1.matchGroupName)(part) == null);
166    if (a.screen === 'index') {
167        aParts.push('index');
168    }
169    const bParts = b.pattern.split('/').filter((part) => (0, matchers_1.matchGroupName)(part) == null);
170    if (b.screen === 'index') {
171        bParts.push('index');
172    }
173    for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
174        // if b is longer, b get higher priority
175        if (aParts[i] == null) {
176            return 1;
177        }
178        // if a is longer, a get higher priority
179        if (bParts[i] == null) {
180            return -1;
181        }
182        const aWildCard = aParts[i].startsWith('*');
183        const bWildCard = bParts[i].startsWith('*');
184        // if both are wildcard we compare next component
185        if (aWildCard && bWildCard) {
186            continue;
187        }
188        // if only a is wild card, b get higher priority
189        if (aWildCard) {
190            return 1;
191        }
192        // if only b is wild card, a get higher priority
193        if (bWildCard) {
194            return -1;
195        }
196        const aSlug = aParts[i].startsWith(':');
197        const bSlug = bParts[i].startsWith(':');
198        // if both are wildcard we compare next component
199        if (aSlug && bSlug) {
200            continue;
201        }
202        // if only a is wild card, b get higher priority
203        if (aSlug) {
204            return 1;
205        }
206        // if only b is wild card, a get higher priority
207        if (bSlug) {
208            return -1;
209        }
210    }
211    // Sort initial routes with a higher priority than routes which will push more screens
212    // this ensures shared routes go to the shortest path.
213    if (a.isInitial && !b.isInitial) {
214        return -1;
215    }
216    if (!a.isInitial && b.isInitial) {
217        return 1;
218    }
219    return bParts.length - aParts.length;
220}
221function getStateFromEmptyPathWithConfigs(path, configs, initialRoutes) {
222    // We need to add special handling of empty path so navigation to empty path also works
223    // When handling empty path, we should only look at the root level config
224    // NOTE(EvanBacon): We only care about matching leaf nodes.
225    const leafNodes = configs
226        .filter((config) => !config.hasChildren)
227        .map((value) => {
228        return {
229            ...value,
230            // Collapse all levels of group segments before testing.
231            // This enables `app/(one)/(two)/index.js` to be matched.
232            path: (0, matchers_1.stripGroupSegmentsFromPath)(value.path),
233        };
234    });
235    const match = leafNodes.find((config) =>
236    // NOTE(EvanBacon): Test leaf node index routes that either don't have a regex or match an empty string.
237    config.path === '' && (!config.regex || config.regex.test(''))) ??
238        leafNodes.find((config) =>
239        // NOTE(EvanBacon): Test leaf node dynamic routes that match an empty string.
240        config.path.startsWith(':') && config.regex.test('')) ??
241        // NOTE(EvanBacon): Test leaf node deep dynamic routes that match a slash.
242        // This should be done last to enable dynamic routes having a higher priority.
243        leafNodes.find((config) => config.path.startsWith('*') && config.regex.test('/'));
244    if (!match) {
245        return undefined;
246    }
247    const routes = match.routeNames.map((name) => {
248        if (!match._route) {
249            return { name };
250        }
251        return {
252            name,
253            _route: match._route,
254        };
255    });
256    return createNestedStateObject(path, routes, configs, initialRoutes);
257}
258function getStateFromPathWithConfigs(path, configs, initialRoutes) {
259    const formattedPaths = getUrlWithReactNavigationConcessions(path);
260    if (formattedPaths.nonstandardPathname === '/') {
261        return getStateFromEmptyPathWithConfigs(formattedPaths.inputPathnameWithoutHash, configs, initialRoutes);
262    }
263    // We match the whole path against the regex instead of segments
264    // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
265    const routes = matchAgainstConfigs(formattedPaths.nonstandardPathname, configs);
266    if (routes == null) {
267        return undefined;
268    }
269    // This will always be empty if full path matched
270    return createNestedStateObject(formattedPaths.inputPathnameWithoutHash, routes, configs, initialRoutes);
271}
272const joinPaths = (...paths) => []
273    .concat(...paths.map((p) => p.split('/')))
274    .filter(Boolean)
275    .join('/');
276function matchAgainstConfigs(remaining, configs) {
277    let routes;
278    let remainingPath = remaining;
279    // Go through all configs, and see if the next path segment matches our regex
280    for (const config of configs) {
281        if (!config.regex) {
282            continue;
283        }
284        const match = remainingPath.match(config.regex);
285        // If our regex matches, we need to extract params from the path
286        if (!match) {
287            continue;
288        }
289        // TODO: Add support for wildcard routes
290        const matchedParams = config.pattern
291            ?.split('/')
292            .filter((p) => p.match(/^[:*]/))
293            .reduce((acc, p, i) => {
294            if (p.match(/^\*/)) {
295                return {
296                    ...acc,
297                    [p]: match[(i + 1) * 2], //?.replace(/\//, ""),
298                };
299            }
300            return Object.assign(acc, {
301                // The param segments appear every second item starting from 2 in the regex match result.
302                // This will only work if we ensure groups aren't included in the match.
303                [p]: match[(i + 1) * 2]?.replace(/\//, ''),
304            });
305        }, {});
306        const routeFromName = (name) => {
307            const config = configs.find((c) => c.screen === name);
308            if (!config?.path) {
309                return { name };
310            }
311            const segments = config.path.split('/');
312            const params = {};
313            segments
314                .filter((p) => p.match(/^[:*]/))
315                .forEach((p) => {
316                let value = matchedParams[p];
317                if (value) {
318                    if (p.match(/^\*/)) {
319                        // Convert to an array before providing as a route.
320                        value = value?.split('/').filter(Boolean);
321                    }
322                    const key = p.replace(/^[:*]/, '').replace(/\?$/, '');
323                    params[key] = config.parse?.[key] ? config.parse[key](value) : value;
324                }
325            });
326            if (params && Object.keys(params).length) {
327                return { name, params };
328            }
329            return { name };
330        };
331        routes = config.routeNames.map((name) => {
332            if (!config._route) {
333                return { ...routeFromName(name) };
334            }
335            return {
336                ...routeFromName(name),
337                _route: config._route,
338            };
339        });
340        // TODO(EvanBacon): Maybe we should warn / assert if multiple slugs use the same param name.
341        const combinedParams = routes.reduce((acc, r) => Object.assign(acc, r.params), {});
342        const hasCombinedParams = Object.keys(combinedParams).length > 0;
343        // Combine all params so a route `[foo]/[bar]/other.js` has access to `{ foo, bar }`
344        routes = routes.map((r) => {
345            if (hasCombinedParams) {
346                r.params = combinedParams;
347            }
348            return r;
349        });
350        remainingPath = remainingPath.replace(match[1], '');
351        break;
352    }
353    return routes;
354}
355function equalHeritage(a, b) {
356    if (a.length !== b.length) {
357        return false;
358    }
359    for (let i = 0; i < a.length; i++) {
360        if (a[i].localeCompare(b[i]) !== 0) {
361            return false;
362        }
363    }
364    return true;
365}
366const createNormalizedConfigs = (screen, routeConfig, routeNames = [], initials = [], parentScreens = [], parentPattern) => {
367    const configs = [];
368    routeNames.push(screen);
369    parentScreens.push(screen);
370    const config = routeConfig[screen];
371    if (typeof config === 'string') {
372        // TODO: This should never happen with the addition of `_route`
373        // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
374        const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
375        configs.push(createConfigItem(screen, routeNames, pattern, config, false));
376    }
377    else if (typeof config === 'object') {
378        let pattern;
379        const { _route } = config;
380        // if an object is specified as the value (e.g. Foo: { ... }),
381        // it can have `path` property and
382        // it could have `screens` prop which has nested configs
383        if (typeof config.path === 'string') {
384            if (config.exact && config.path === undefined) {
385                throw new Error("A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. `path: ''`.");
386            }
387            pattern =
388                config.exact !== true
389                    ? joinPaths(parentPattern || '', config.path || '')
390                    : config.path || '';
391            configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.screens ? !!Object.keys(config.screens)?.length : false, config.parse, _route));
392        }
393        if (config.screens) {
394            // property `initialRouteName` without `screens` has no purpose
395            if (config.initialRouteName) {
396                initials.push({
397                    initialRouteName: config.initialRouteName,
398                    parentScreens,
399                });
400            }
401            Object.keys(config.screens).forEach((nestedConfig) => {
402                const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [...parentScreens], pattern ?? parentPattern);
403                configs.push(...result);
404            });
405        }
406    }
407    routeNames.pop();
408    return configs;
409};
410function formatRegexPattern(it) {
411    // Allow spaces in file path names.
412    it = it.replace(' ', '%20');
413    if (it.startsWith(':')) {
414        // TODO: Remove unused match group
415        return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
416    }
417    else if (it.startsWith('*')) {
418        return `((.*\\/)${it.endsWith('?') ? '?' : ''})`;
419    }
420    // Strip groups from the matcher
421    if ((0, matchers_1.matchGroupName)(it) != null) {
422        // Groups are optional segments
423        // this enables us to match `/bar` and `/(foo)/bar` for the same route
424        // NOTE(EvanBacon): Ignore this match in the regex to avoid capturing the group
425        return `(?:${(0, escape_string_regexp_1.default)(it)}\\/)?`;
426    }
427    return (0, escape_string_regexp_1.default)(it) + `\\/`;
428}
429const createConfigItem = (screen, routeNames, pattern, path, hasChildren, parse, _route) => {
430    // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
431    pattern = pattern.split('/').filter(Boolean).join('/');
432    const regex = pattern
433        ? new RegExp(`^(${pattern.split('/').map(formatRegexPattern).join('')})$`)
434        : undefined;
435    return {
436        screen,
437        regex,
438        pattern,
439        path,
440        // The routeNames array is mutated, so copy it to keep the current state
441        routeNames: [...routeNames],
442        parse,
443        userReadableName: [...routeNames.slice(0, -1), path || screen].join('/'),
444        hasChildren: !!hasChildren,
445        _route,
446    };
447};
448const findParseConfigForRoute = (routeName, routeConfigs) => {
449    for (const config of routeConfigs) {
450        if (routeName === config.routeNames[config.routeNames.length - 1]) {
451            return config.parse;
452        }
453    }
454    return undefined;
455};
456// Try to find an initial route connected with the one passed
457const findInitialRoute = (routeName, parentScreens, initialRoutes) => {
458    for (const config of initialRoutes) {
459        if (equalHeritage(parentScreens, config.parentScreens)) {
460            // If the parents are the same but the route name doesn't match the initial route
461            // then we return the initial route.
462            return routeName !== config.initialRouteName ? config.initialRouteName : undefined;
463        }
464    }
465    return undefined;
466};
467// returns state object with values depending on whether
468// it is the end of state and if there is initialRoute for this level
469const createStateObject = (initialRoute, route, isEmpty) => {
470    if (isEmpty) {
471        if (initialRoute) {
472            return {
473                index: 1,
474                routes: [{ name: initialRoute }, route],
475            };
476        }
477        return {
478            routes: [route],
479        };
480    }
481    if (initialRoute) {
482        return {
483            index: 1,
484            routes: [{ name: initialRoute }, { ...route, state: { routes: [] } }],
485        };
486    }
487    return {
488        routes: [{ ...route, state: { routes: [] } }],
489    };
490};
491const createNestedStateObject = (path, routes, routeConfigs, initialRoutes) => {
492    let route = routes.shift();
493    const parentScreens = [];
494    let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
495    parentScreens.push(route.name);
496    const state = createStateObject(initialRoute, route, routes.length === 0);
497    if (routes.length > 0) {
498        let nestedState = state;
499        while ((route = routes.shift())) {
500            initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
501            const nestedStateIndex = nestedState.index || nestedState.routes.length - 1;
502            nestedState.routes[nestedStateIndex].state = createStateObject(initialRoute, route, routes.length === 0);
503            if (routes.length > 0) {
504                nestedState = nestedState.routes[nestedStateIndex].state;
505            }
506            parentScreens.push(route.name);
507        }
508    }
509    route = (0, findFocusedRoute_1.findFocusedRoute)(state);
510    // Remove groups from the path while preserving a trailing slash.
511    route.path = (0, matchers_1.stripGroupSegmentsFromPath)(path);
512    const params = parseQueryParams(route.path, findParseConfigForRoute(route.name, routeConfigs));
513    if (params) {
514        const resolvedParams = { ...route.params, ...params };
515        if (Object.keys(resolvedParams).length > 0) {
516            route.params = resolvedParams;
517        }
518        else {
519            delete route.params;
520        }
521    }
522    return state;
523};
524const parseQueryParams = (path, parseConfig) => {
525    const query = path.split('?')[1];
526    const params = queryString.parse(query);
527    if (parseConfig) {
528        Object.keys(params).forEach((name) => {
529            if (Object.hasOwnProperty.call(parseConfig, name) && typeof params[name] === 'string') {
530                params[name] = parseConfig[name](params[name]);
531            }
532        });
533    }
534    return Object.keys(params).length ? params : undefined;
535};
536const basePathCache = new Map();
537function getBasePathRegex(basePath) {
538    if (basePathCache.has(basePath)) {
539        return basePathCache.get(basePath);
540    }
541    const regex = new RegExp(`^\\/?${(0, escape_string_regexp_1.default)(basePath)}`, 'g');
542    basePathCache.set(basePath, regex);
543    return regex;
544}
545function stripBasePath(path, basePath = expo_constants_1.default.expoConfig?.experiments?.basePath) {
546    if (process.env.NODE_ENV !== 'development') {
547        if (basePath) {
548            const reg = getBasePathRegex(basePath);
549            return path.replace(/^\/+/g, '/').replace(reg, '');
550        }
551    }
552    return path;
553}
554exports.stripBasePath = stripBasePath;
555//# sourceMappingURL=getStateFromPath.js.map