UNPKG

15.5 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.default = getStateFromPath;
7
8var _escapeStringRegexp = _interopRequireDefault(require("escape-string-regexp"));
9
10var queryString = _interopRequireWildcard(require("query-string"));
11
12var _findFocusedRoute = _interopRequireDefault(require("./findFocusedRoute"));
13
14var _validatePathConfig = _interopRequireDefault(require("./validatePathConfig"));
15
16function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
17
18function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
19
20function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
22/**
23 * Utility to parse a path string to initial state object accepted by the container.
24 * This is useful for deep linking when we need to handle the incoming URL.
25 *
26 * @example
27 * ```js
28 * getStateFromPath(
29 * '/chat/jane/42',
30 * {
31 * screens: {
32 * Chat: {
33 * path: 'chat/:author/:id',
34 * parse: { id: Number }
35 * }
36 * }
37 * }
38 * )
39 * ```
40 * @param path Path string to parse and convert, e.g. /foo/bar?count=42.
41 * @param options Extra options to fine-tune how to parse the path.
42 */
43function getStateFromPath(path, options) {
44 if (options) {
45 (0, _validatePathConfig.default)(options);
46 }
47
48 let initialRoutes = [];
49
50 if (options !== null && options !== void 0 && options.initialRouteName) {
51 initialRoutes.push({
52 initialRouteName: options.initialRouteName,
53 parentScreens: []
54 });
55 }
56
57 const screens = options === null || options === void 0 ? void 0 : options.screens;
58 let remaining = path.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
59 .replace(/^\//, '') // Remove extra leading slash
60 .replace(/\?.*$/, ''); // Remove query params which we will handle later
61 // Make sure there is a trailing slash
62
63 remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
64
65 if (screens === undefined) {
66 // When no config is specified, use the path segments as route names
67 const routes = remaining.split('/').filter(Boolean).map(segment => {
68 const name = decodeURIComponent(segment);
69 return {
70 name
71 };
72 });
73
74 if (routes.length) {
75 return createNestedStateObject(path, routes, initialRoutes);
76 }
77
78 return undefined;
79 } // Create a normalized configs array which will be easier to use
80
81
82 const configs = [].concat(...Object.keys(screens).map(key => createNormalizedConfigs(key, screens, [], initialRoutes, []))).sort((a, b) => {
83 // Sort config so that:
84 // - the most exhaustive ones are always at the beginning
85 // - patterns with wildcard are always at the end
86 // If 2 patterns are same, move the one with less route names up
87 // This is an error state, so it's only useful for consistent error messages
88 if (a.pattern === b.pattern) {
89 return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
90 } // If one of the patterns starts with the other, it's more exhaustive
91 // So move it up
92
93
94 if (a.pattern.startsWith(b.pattern)) {
95 return -1;
96 }
97
98 if (b.pattern.startsWith(a.pattern)) {
99 return 1;
100 }
101
102 const aParts = a.pattern.split('/');
103 const bParts = b.pattern.split('/');
104
105 for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
106 // if b is longer, b get higher priority
107 if (aParts[i] == null) {
108 return 1;
109 } // if a is longer, a get higher priority
110
111
112 if (bParts[i] == null) {
113 return -1;
114 }
115
116 const aWildCard = aParts[i] === '*' || aParts[i].startsWith(':');
117 const bWildCard = bParts[i] === '*' || bParts[i].startsWith(':'); // if both are wildcard we compare next component
118
119 if (aWildCard && bWildCard) {
120 continue;
121 } // if only a is wild card, b get higher priority
122
123
124 if (aWildCard) {
125 return 1;
126 } // if only b is wild card, a get higher priority
127
128
129 if (bWildCard) {
130 return -1;
131 }
132 }
133
134 return bParts.length - aParts.length;
135 }); // Check for duplicate patterns in the config
136
137 configs.reduce((acc, config) => {
138 if (acc[config.pattern]) {
139 const a = acc[config.pattern].routeNames;
140 const b = config.routeNames; // It's not a problem if the path string omitted from a inner most screen
141 // For example, it's ok if a path resolves to `A > B > C` or `A > B`
142
143 const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it);
144
145 if (!intersects) {
146 throw new Error(`Found conflicting screens with the same pattern. The pattern '${config.pattern}' resolves to both '${a.join(' > ')}' and '${b.join(' > ')}'. Patterns must be unique and cannot resolve to more than one screen.`);
147 }
148 }
149
150 return Object.assign(acc, {
151 [config.pattern]: config
152 });
153 }, {});
154
155 if (remaining === '/') {
156 // We need to add special handling of empty path so navigation to empty path also works
157 // When handling empty path, we should only look at the root level config
158 const match = configs.find(config => config.path === '' && config.routeNames.every( // Make sure that none of the parent configs have a non-empty path defined
159 name => {
160 var _configs$find;
161
162 return !((_configs$find = configs.find(c => c.screen === name)) !== null && _configs$find !== void 0 && _configs$find.path);
163 }));
164
165 if (match) {
166 return createNestedStateObject(path, match.routeNames.map(name => ({
167 name
168 })), initialRoutes, configs);
169 }
170
171 return undefined;
172 }
173
174 let result;
175 let current; // We match the whole path against the regex instead of segments
176 // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
177
178 const {
179 routes,
180 remainingPath
181 } = matchAgainstConfigs(remaining, configs.map(c => ({ ...c,
182 // Add `$` to the regex to make sure it matches till end of the path and not just beginning
183 regex: c.regex ? new RegExp(c.regex.source + '$') : undefined
184 })));
185
186 if (routes !== undefined) {
187 // This will always be empty if full path matched
188 current = createNestedStateObject(path, routes, initialRoutes, configs);
189 remaining = remainingPath;
190 result = current;
191 }
192
193 if (current == null || result == null) {
194 return undefined;
195 }
196
197 return result;
198}
199
200const joinPaths = function () {
201 for (var _len = arguments.length, paths = new Array(_len), _key = 0; _key < _len; _key++) {
202 paths[_key] = arguments[_key];
203 }
204
205 return [].concat(...paths.map(p => p.split('/'))).filter(Boolean).join('/');
206};
207
208const matchAgainstConfigs = (remaining, configs) => {
209 let routes;
210 let remainingPath = remaining; // Go through all configs, and see if the next path segment matches our regex
211
212 for (const config of configs) {
213 if (!config.regex) {
214 continue;
215 }
216
217 const match = remainingPath.match(config.regex); // If our regex matches, we need to extract params from the path
218
219 if (match) {
220 var _config$pattern;
221
222 const matchedParams = (_config$pattern = config.pattern) === null || _config$pattern === void 0 ? void 0 : _config$pattern.split('/').filter(p => p.startsWith(':')).reduce((acc, p, i) => Object.assign(acc, {
223 // The param segments appear every second item starting from 2 in the regex match result
224 [p]: match[(i + 1) * 2].replace(/\//, '')
225 }), {});
226 routes = config.routeNames.map(name => {
227 var _config$path;
228
229 const config = configs.find(c => c.screen === name);
230 const params = config === null || config === void 0 ? void 0 : (_config$path = config.path) === null || _config$path === void 0 ? void 0 : _config$path.split('/').filter(p => p.startsWith(':')).reduce((acc, p) => {
231 const value = matchedParams[p];
232
233 if (value) {
234 var _config$parse;
235
236 const key = p.replace(/^:/, '').replace(/\?$/, '');
237 acc[key] = (_config$parse = config.parse) !== null && _config$parse !== void 0 && _config$parse[key] ? config.parse[key](value) : value;
238 }
239
240 return acc;
241 }, {});
242
243 if (params && Object.keys(params).length) {
244 return {
245 name,
246 params
247 };
248 }
249
250 return {
251 name
252 };
253 });
254 remainingPath = remainingPath.replace(match[1], '');
255 break;
256 }
257 }
258
259 return {
260 routes,
261 remainingPath
262 };
263};
264
265const createNormalizedConfigs = function (screen, routeConfig) {
266 let routeNames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
267 let initials = arguments.length > 3 ? arguments[3] : undefined;
268 let parentScreens = arguments.length > 4 ? arguments[4] : undefined;
269 let parentPattern = arguments.length > 5 ? arguments[5] : undefined;
270 const configs = [];
271 routeNames.push(screen);
272 parentScreens.push(screen); // @ts-expect-error: we can't strongly typecheck this for now
273
274 const config = routeConfig[screen];
275
276 if (typeof config === 'string') {
277 // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
278 const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
279 configs.push(createConfigItem(screen, routeNames, pattern, config));
280 } else if (typeof config === 'object') {
281 let pattern; // if an object is specified as the value (e.g. Foo: { ... }),
282 // it can have `path` property and
283 // it could have `screens` prop which has nested configs
284
285 if (typeof config.path === 'string') {
286 if (config.exact && config.path === undefined) {
287 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: ''`.");
288 }
289
290 pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || '';
291 configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.parse));
292 }
293
294 if (config.screens) {
295 // property `initialRouteName` without `screens` has no purpose
296 if (config.initialRouteName) {
297 initials.push({
298 initialRouteName: config.initialRouteName,
299 parentScreens
300 });
301 }
302
303 Object.keys(config.screens).forEach(nestedConfig => {
304 var _pattern;
305
306 const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [...parentScreens], (_pattern = pattern) !== null && _pattern !== void 0 ? _pattern : parentPattern);
307 configs.push(...result);
308 });
309 }
310 }
311
312 routeNames.pop();
313 return configs;
314};
315
316const createConfigItem = (screen, routeNames, pattern, path, parse) => {
317 // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
318 pattern = pattern.split('/').filter(Boolean).join('/');
319 const regex = pattern ? new RegExp(`^(${pattern.split('/').map(it => {
320 if (it.startsWith(':')) {
321 return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
322 }
323
324 return `${it === '*' ? '.*' : (0, _escapeStringRegexp.default)(it)}\\/`;
325 }).join('')})`) : undefined;
326 return {
327 screen,
328 regex,
329 pattern,
330 path,
331 // The routeNames array is mutated, so copy it to keep the current state
332 routeNames: [...routeNames],
333 parse
334 };
335};
336
337const findParseConfigForRoute = (routeName, flatConfig) => {
338 for (const config of flatConfig) {
339 if (routeName === config.routeNames[config.routeNames.length - 1]) {
340 return config.parse;
341 }
342 }
343
344 return undefined;
345}; // Try to find an initial route connected with the one passed
346
347
348const findInitialRoute = (routeName, parentScreens, initialRoutes) => {
349 for (const config of initialRoutes) {
350 if (parentScreens.length === config.parentScreens.length) {
351 let sameParents = true;
352
353 for (let i = 0; i < parentScreens.length; i++) {
354 if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
355 sameParents = false;
356 break;
357 }
358 }
359
360 if (sameParents) {
361 return routeName !== config.initialRouteName ? config.initialRouteName : undefined;
362 }
363 }
364 }
365
366 return undefined;
367}; // returns state object with values depending on whether
368// it is the end of state and if there is initialRoute for this level
369
370
371const createStateObject = (initialRoute, route, isEmpty) => {
372 if (isEmpty) {
373 if (initialRoute) {
374 return {
375 index: 1,
376 routes: [{
377 name: initialRoute
378 }, route]
379 };
380 } else {
381 return {
382 routes: [route]
383 };
384 }
385 } else {
386 if (initialRoute) {
387 return {
388 index: 1,
389 routes: [{
390 name: initialRoute
391 }, { ...route,
392 state: {
393 routes: []
394 }
395 }]
396 };
397 } else {
398 return {
399 routes: [{ ...route,
400 state: {
401 routes: []
402 }
403 }]
404 };
405 }
406 }
407};
408
409const createNestedStateObject = (path, routes, initialRoutes, flatConfig) => {
410 let state;
411 let route = routes.shift();
412 const parentScreens = [];
413 let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
414 parentScreens.push(route.name);
415 state = createStateObject(initialRoute, route, routes.length === 0);
416
417 if (routes.length > 0) {
418 let nestedState = state;
419
420 while (route = routes.shift()) {
421 initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
422 const nestedStateIndex = nestedState.index || nestedState.routes.length - 1;
423 nestedState.routes[nestedStateIndex].state = createStateObject(initialRoute, route, routes.length === 0);
424
425 if (routes.length > 0) {
426 nestedState = nestedState.routes[nestedStateIndex].state;
427 }
428
429 parentScreens.push(route.name);
430 }
431 }
432
433 route = (0, _findFocusedRoute.default)(state);
434 route.path = path;
435 const params = parseQueryParams(path, flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined);
436
437 if (params) {
438 route.params = { ...route.params,
439 ...params
440 };
441 }
442
443 return state;
444};
445
446const parseQueryParams = (path, parseConfig) => {
447 const query = path.split('?')[1];
448 const params = queryString.parse(query);
449
450 if (parseConfig) {
451 Object.keys(params).forEach(name => {
452 if (Object.hasOwnProperty.call(parseConfig, name) && typeof params[name] === 'string') {
453 params[name] = parseConfig[name](params[name]);
454 }
455 });
456 }
457
458 return Object.keys(params).length ? params : undefined;
459};
460//# sourceMappingURL=getStateFromPath.js.map
\No newline at end of file