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.appendBasePath = exports.deepEqual = exports.getPathDataFromState = void 0;
30const core_1 = require("@react-navigation/core");
31const expo_constants_1 = __importDefault(require("expo-constants"));
32const queryString = __importStar(require("query-string"));
33const matchers_1 = require("../matchers");
34const DEFAULT_SCREENS = {};
35const getActiveRoute = (state) => {
36    const route = typeof state.index === 'number'
37        ? state.routes[state.index]
38        : state.routes[state.routes.length - 1];
39    if (route.state) {
40        return getActiveRoute(route.state);
41    }
42    if (route && isInvalidParams(route.params)) {
43        return getActiveRoute(createFakeState(route.params));
44    }
45    return route;
46};
47function createFakeState(params) {
48    return {
49        stale: false,
50        type: 'UNKNOWN',
51        key: 'UNKNOWN',
52        index: 0,
53        routeNames: [],
54        routes: [
55            {
56                key: 'UNKNOWN',
57                name: params.screen,
58                params: params.params,
59                path: params.path,
60            },
61        ],
62    };
63}
64function segmentMatchesConvention(segment) {
65    return (segment === 'index' ||
66        (0, matchers_1.matchDynamicName)(segment) != null ||
67        (0, matchers_1.matchGroupName)(segment) != null ||
68        (0, matchers_1.matchDeepDynamicRouteName)(segment) != null);
69}
70function encodeURIComponentPreservingBrackets(str) {
71    return encodeURIComponent(str).replace(/%5B/g, '[').replace(/%5D/g, ']');
72}
73/**
74 * Utility to serialize a navigation state object to a path string.
75 *
76 * @example
77 * ```js
78 * getPathFromState(
79 *   {
80 *     routes: [
81 *       {
82 *         name: 'Chat',
83 *         params: { author: 'Jane', id: 42 },
84 *       },
85 *     ],
86 *   },
87 *   {
88 *     screens: {
89 *       Chat: {
90 *         path: 'chat/:author/:id',
91 *         stringify: { author: author => author.toLowerCase() }
92 *       }
93 *     }
94 *   }
95 * )
96 * ```
97 *
98 * @param state Navigation state to serialize.
99 * @param options Extra options to fine-tune how to serialize the path.
100 * @returns Path representing the state, e.g. /foo/bar?count=42.
101 */
102function getPathFromState(state, _options) {
103    return getPathDataFromState(state, _options).path;
104}
105exports.default = getPathFromState;
106function getPathDataFromState(state, _options = { screens: DEFAULT_SCREENS }) {
107    if (state == null) {
108        throw Error("Got 'undefined' for the navigation state. You must pass a valid state object.");
109    }
110    const { preserveGroups, preserveDynamicRoutes, ...options } = _options;
111    (0, core_1.validatePathConfig)(options);
112    // Expo Router disallows usage without a linking config.
113    if (Object.is(options.screens, DEFAULT_SCREENS)) {
114        throw Error("You must pass a 'screens' object to 'getPathFromState' to generate a path.");
115    }
116    return getPathFromResolvedState(JSON.parse(JSON.stringify(state)),
117    // Create a normalized configs object which will be easier to use
118    createNormalizedConfigs(options.screens), { preserveGroups, preserveDynamicRoutes });
119}
120exports.getPathDataFromState = getPathDataFromState;
121function processParamsWithUserSettings(configItem, params) {
122    const stringify = configItem?.stringify;
123    return Object.fromEntries(Object.entries(params).map(([key, value]) => [
124        key,
125        // TODO: Strip nullish values here.
126        stringify?.[key]
127            ? stringify[key](value)
128            : // Preserve rest params
129                Array.isArray(value)
130                    ? value
131                    : String(value),
132    ]));
133}
134function deepEqual(a, b) {
135    if (a === b) {
136        return true;
137    }
138    if (Array.isArray(a) && Array.isArray(b)) {
139        if (a.length !== b.length) {
140            return false;
141        }
142        for (let i = 0; i < a.length; i++) {
143            if (!deepEqual(a[i], b[i])) {
144                return false;
145            }
146        }
147        return true;
148    }
149    if (typeof a === 'object' && typeof b === 'object') {
150        const keysA = Object.keys(a);
151        const keysB = Object.keys(b);
152        if (keysA.length !== keysB.length) {
153            return false;
154        }
155        for (const key of keysA) {
156            if (!deepEqual(a[key], b[key])) {
157                return false;
158            }
159        }
160        return true;
161    }
162    return false;
163}
164exports.deepEqual = deepEqual;
165function walkConfigItems(route, focusedRoute, configs, { preserveDynamicRoutes, }) {
166    // NOTE(EvanBacon): Fill in current route using state that was passed as params.
167    if (!route.state && isInvalidParams(route.params)) {
168        route.state = createFakeState(route.params);
169    }
170    let pattern = null;
171    let focusedParams;
172    const collectedParams = {};
173    while (route.name in configs) {
174        const configItem = configs[route.name];
175        const inputPattern = configItem.pattern;
176        if (inputPattern == null) {
177            // This should never happen in Expo Router.
178            throw new Error('Unexpected: No pattern found for route ' + route.name);
179        }
180        pattern = inputPattern;
181        if (route.params) {
182            const params = processParamsWithUserSettings(configItem, route.params);
183            // TODO: Does this need to be a null check?
184            if (pattern) {
185                Object.assign(collectedParams, params);
186            }
187            if (deepEqual(focusedRoute, route)) {
188                if (preserveDynamicRoutes) {
189                    focusedParams = params;
190                }
191                else {
192                    // If this is the focused route, keep the params for later use
193                    // We save it here since it's been stringified already
194                    focusedParams = getParamsWithConventionsCollapsed({
195                        params,
196                        pattern,
197                        routeName: route.name,
198                    });
199                }
200            }
201        }
202        if (!route.state && isInvalidParams(route.params)) {
203            route.state = createFakeState(route.params);
204        }
205        // If there is no `screens` property or no nested state, we return pattern
206        if (!configItem.screens || route.state === undefined) {
207            if (configItem.initialRouteName &&
208                configItem.screens &&
209                configItem.initialRouteName in configItem.screens &&
210                configItem.screens[configItem.initialRouteName]?.pattern) {
211                const initialRouteConfig = configItem.screens[configItem.initialRouteName];
212                // NOTE(EvanBacon): Big hack to support initial route changes in tab bars.
213                pattern = initialRouteConfig.pattern;
214                if (focusedParams) {
215                    if (!preserveDynamicRoutes) {
216                        // If this is the focused route, keep the params for later use
217                        // We save it here since it's been stringified already
218                        focusedParams = getParamsWithConventionsCollapsed({
219                            params: focusedParams,
220                            pattern,
221                            routeName: route.name,
222                        });
223                    }
224                }
225            }
226            break;
227        }
228        const index = route.state.index ?? route.state.routes.length - 1;
229        const nextRoute = route.state.routes[index];
230        const nestedScreens = configItem.screens;
231        // if there is config for next route name, we go deeper
232        if (nestedScreens && nextRoute.name in nestedScreens) {
233            route = nextRoute;
234            configs = nestedScreens;
235        }
236        else {
237            // If not, there is no sense in going deeper in config
238            break;
239        }
240    }
241    if (pattern == null) {
242        throw new Error(`No pattern found for route "${route.name}". Options are: ${Object.keys(configs).join(', ')}.`);
243    }
244    if (pattern && !focusedParams && focusedRoute.params) {
245        if (preserveDynamicRoutes) {
246            focusedParams = focusedRoute.params;
247        }
248        else {
249            // If this is the focused route, keep the params for later use
250            // We save it here since it's been stringified already
251            focusedParams = getParamsWithConventionsCollapsed({
252                params: focusedRoute.params,
253                pattern,
254                routeName: route.name,
255            });
256        }
257        Object.assign(focusedParams, collectedParams);
258    }
259    return {
260        pattern,
261        nextRoute: route,
262        focusedParams,
263        params: collectedParams,
264    };
265}
266function getPathFromResolvedState(state, configs, { preserveGroups, preserveDynamicRoutes, }) {
267    let path = '';
268    let current = state;
269    const allParams = {};
270    while (current) {
271        path += '/';
272        // Make mutable copies to ensure we don't leak state outside of the function.
273        const route = current.routes[current.index ?? 0];
274        // NOTE(EvanBacon): Fill in current route using state that was passed as params.
275        // if (isInvalidParams(route.params)) {
276        if (!route.state && isInvalidParams(route.params)) {
277            route.state = createFakeState(route.params);
278        }
279        const { pattern, params, nextRoute, focusedParams } = walkConfigItems(route, getActiveRoute(current), { ...configs }, { preserveDynamicRoutes });
280        Object.assign(allParams, params);
281        path += getPathWithConventionsCollapsed({
282            pattern,
283            routePath: nextRoute.path,
284            params: allParams,
285            initialRouteName: configs[nextRoute.name]?.initialRouteName,
286            preserveGroups,
287            preserveDynamicRoutes,
288        });
289        if (nextRoute.state &&
290            // NOTE(EvanBacon): The upstream implementation allows for sending in synthetic states (states that weren't generated by `getStateFromPath`)
291            // and any invalid routes will simply be ignored.
292            // Because of this, we need to check if the next route is valid before continuing, otherwise our more strict
293            // implementation will throw an error.
294            configs[nextRoute.state.routes?.[nextRoute.state?.index ?? 0]?.name]) {
295            // Continue looping with the next state if available.
296            current = nextRoute.state;
297        }
298        else {
299            // Finished crawling state.
300            // Check for query params before exiting.
301            if (focusedParams) {
302                for (const param in focusedParams) {
303                    // TODO: This is not good. We shouldn't squat strings named "undefined".
304                    if (focusedParams[param] === 'undefined') {
305                        // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
306                        delete focusedParams[param];
307                    }
308                }
309                const query = queryString.stringify(focusedParams, { sort: false });
310                if (query) {
311                    path += `?${query}`;
312                }
313            }
314            break;
315        }
316    }
317    return { path: appendBasePath(basicSanitizePath(path)), params: decodeParams(allParams) };
318}
319function decodeParams(params) {
320    const parsed = {};
321    for (const [key, value] of Object.entries(params)) {
322        parsed[key] = decodeURIComponent(value);
323    }
324    return parsed;
325}
326function getPathWithConventionsCollapsed({ pattern, routePath, params, preserveGroups, preserveDynamicRoutes, initialRouteName, }) {
327    const segments = pattern.split('/');
328    return segments
329        .map((p, i) => {
330        const name = getParamName(p);
331        // We don't know what to show for wildcard patterns
332        // Showing the route name seems ok, though whatever we show here will be incorrect
333        // Since the page doesn't actually exist
334        if (p.startsWith('*')) {
335            if (preserveDynamicRoutes) {
336                return `[...${name}]`;
337            }
338            if (params[name]) {
339                if (Array.isArray(params[name])) {
340                    return params[name].join('/');
341                }
342                return params[name];
343            }
344            if (i === 0) {
345                // This can occur when a wildcard matches all routes and the given path was `/`.
346                return routePath;
347            }
348            // remove existing segments from route.path and return it
349            // this is used for nested wildcard routes. Without this, the path would add
350            // all nested segments to the beginning of the wildcard route.
351            return routePath
352                ?.split('/')
353                .slice(i + 1)
354                .join('/');
355        }
356        // If the path has a pattern for a param, put the param in the path
357        if (p.startsWith(':')) {
358            if (preserveDynamicRoutes) {
359                return `[${name}]`;
360            }
361            // Optional params without value assigned in route.params should be ignored
362            return params[name];
363        }
364        if (!preserveGroups && (0, matchers_1.matchGroupName)(p) != null) {
365            // When the last part is a group it could be a shared URL
366            // if the route has an initialRouteName defined, then we should
367            // use that as the component path as we can assume it will be shown.
368            if (segments.length - 1 === i) {
369                if (initialRouteName) {
370                    // Return an empty string if the init route is ambiguous.
371                    if (segmentMatchesConvention(initialRouteName)) {
372                        return '';
373                    }
374                    return encodeURIComponentPreservingBrackets(initialRouteName);
375                }
376            }
377            return '';
378        }
379        // Preserve dynamic syntax for rehydration
380        return encodeURIComponentPreservingBrackets(p);
381    })
382        .map((v) => v ?? '')
383        .join('/');
384}
385/** Given a set of query params and a pattern with possible conventions, collapse the conventions and return the remaining params. */
386function getParamsWithConventionsCollapsed({ pattern, routeName, params, }) {
387    const processedParams = { ...params };
388    // Remove the params present in the pattern since we'll only use the rest for query string
389    const segments = pattern.split('/');
390    // Dynamic Routes
391    segments
392        .filter((segment) => segment.startsWith(':'))
393        .forEach((segment) => {
394        const name = getParamName(segment);
395        delete processedParams[name];
396    });
397    // Deep Dynamic Routes
398    if (segments.some((segment) => segment.startsWith('*'))) {
399        // NOTE(EvanBacon): Drop the param name matching the wildcard route name -- this is specific to Expo Router.
400        const name = (0, matchers_1.matchDeepDynamicRouteName)(routeName) ?? routeName;
401        delete processedParams[name];
402    }
403    return processedParams;
404}
405// Remove multiple as well as trailing slashes
406function basicSanitizePath(path) {
407    // Remove duplicate slashes like `foo//bar` -> `foo/bar`
408    const simplifiedPath = path.replace(/\/+/g, '/');
409    if (simplifiedPath.length <= 1) {
410        return simplifiedPath;
411    }
412    // Remove trailing slash like `foo/bar/` -> `foo/bar`
413    return simplifiedPath.replace(/\/$/, '');
414}
415// TODO: Make StackRouter not do this...
416// Detect if the params came from StackRouter using `params` to pass around internal state.
417function isInvalidParams(params) {
418    if (!params) {
419        return false;
420    }
421    if ('params' in params && typeof params.params === 'object' && !!params.params) {
422        return true;
423    }
424    return ('initial' in params &&
425        typeof params.initial === 'boolean' &&
426        // "path" in params &&
427        'screen' in params);
428}
429const getParamName = (pattern) => pattern.replace(/^[:*]/, '').replace(/\?$/, '');
430const joinPaths = (...paths) => []
431    .concat(...paths.map((p) => p.split('/')))
432    .filter(Boolean)
433    .join('/');
434const createConfigItem = (config, parentPattern) => {
435    if (typeof config === 'string') {
436        // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
437        const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
438        return { pattern };
439    }
440    if (config.exact && config.path === undefined) {
441        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: ''`.");
442    }
443    // If an object is specified as the value (e.g. Foo: { ... }),
444    // It can have `path` property and `screens` prop which has nested configs
445    const pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || '';
446    const screens = config.screens ? createNormalizedConfigs(config.screens, pattern) : undefined;
447    return {
448        // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
449        pattern: pattern?.split('/').filter(Boolean).join('/'),
450        stringify: config.stringify,
451        screens,
452        initialRouteName: config.initialRouteName,
453    };
454};
455const createNormalizedConfigs = (options, pattern) => Object.fromEntries(Object.entries(options).map(([name, c]) => [name, createConfigItem(c, pattern)]));
456function appendBasePath(path, assetPrefix = expo_constants_1.default.expoConfig?.experiments?.basePath) {
457    if (process.env.NODE_ENV !== 'development') {
458        if (assetPrefix) {
459            return `/${assetPrefix.replace(/^\/+/, '').replace(/\/$/, '')}${path}`;
460        }
461    }
462    return path;
463}
464exports.appendBasePath = appendBasePath;
465//# sourceMappingURL=getPathFromState.js.map