create-expo-cljs-app
Version:
Create a react native application with Expo and Shadow-CLJS!
500 lines (405 loc) • 17 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = getStateFromPath;
var _escapeStringRegexp = _interopRequireDefault(require("escape-string-regexp"));
var queryString = _interopRequireWildcard(require("query-string"));
var _checkLegacyPathConfig = _interopRequireDefault(require("./checkLegacyPathConfig"));
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (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; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Utility to parse a path string to initial state object accepted by the container.
* This is useful for deep linking when we need to handle the incoming URL.
*
* @example
* ```js
* getStateFromPath(
* '/chat/jane/42',
* {
* screens: {
* Chat: {
* path: 'chat/:author/:id',
* parse: { id: Number }
* }
* }
* }
* )
* ```
* @param path Path string to parse and convert, e.g. /foo/bar?count=42.
* @param options Extra options to fine-tune how to parse the path.
*/
function getStateFromPath(path, options) {
const [legacy, compatOptions] = (0, _checkLegacyPathConfig.default)(options);
let initialRoutes = [];
if (compatOptions !== null && compatOptions !== void 0 && compatOptions.initialRouteName) {
initialRoutes.push({
initialRouteName: compatOptions.initialRouteName,
connectedRoutes: Object.keys(compatOptions.screens)
});
}
const screens = compatOptions === null || compatOptions === void 0 ? void 0 : compatOptions.screens;
let remaining = path.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*$/, ''); // Remove query params which we will handle later
// Make sure there is a trailing slash
remaining = remaining.endsWith('/') ? remaining : "".concat(remaining, "/");
if (screens === undefined) {
// When no config is specified, use the path segments as route names
const routes = remaining.split('/').filter(Boolean).map((segment, i, self) => {
const name = decodeURIComponent(segment);
if (i === self.length - 1) {
return {
name,
params: parseQueryParams(path)
};
}
return {
name
};
});
if (routes.length) {
return createNestedStateObject(routes, initialRoutes);
}
return undefined;
} // Create a normalized configs array which will be easier to use
const configs = [].concat(...Object.keys(screens).map(key => createNormalizedConfigs(legacy, key, screens, [], initialRoutes))).sort((a, b) => {
// Sort config so that:
// - the most exhaustive ones are always at the beginning
// - patterns with wildcard are always at the end
// If 2 patterns are same, move the one with less route names up
// This is an error state, so it's only useful for consistent error messages
if (a.pattern === b.pattern) {
return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
} // If one of the patterns starts with the other, it's more exhaustive
// So move it up
if (a.pattern.startsWith(b.pattern)) {
return -1;
}
if (b.pattern.startsWith(a.pattern)) {
return 1;
}
const aParts = a.pattern.split('/');
const bParts = b.pattern.split('/');
const aWildcardIndex = aParts.indexOf('*');
const bWildcardIndex = bParts.indexOf('*'); // If only one of the patterns has a wildcard, move it down in the list
if (aWildcardIndex === -1 && bWildcardIndex !== -1) {
return -1;
}
if (aWildcardIndex !== -1 && bWildcardIndex === -1) {
return 1;
}
if (aWildcardIndex === bWildcardIndex) {
// If `b` has more `/`, it's more exhaustive
// So we move it up in the list
return bParts.length - aParts.length;
} // If the wildcard appears later in the pattern (has higher index), it's more specific
// So we move it up in the list
return bWildcardIndex - aWildcardIndex;
}); // Check for duplicate patterns in the config
configs.reduce((acc, config) => {
if (acc[config.pattern]) {
const a = acc[config.pattern].routeNames;
const b = config.routeNames; // It's not a problem if the path string omitted from a inner most screen
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it);
if (!intersects) {
throw new Error("Found conflicting screens with the same pattern. The pattern '".concat(config.pattern, "' resolves to both '").concat(a.join(' > '), "' and '").concat(b.join(' > '), "'. Patterns must be unique and cannot resolve to more than one screen."));
}
}
return Object.assign(acc, {
[config.pattern]: config
});
}, {});
if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works
// When handling empty path, we should only look at the root level config
const match = configs.find(config => config.path === '' && config.routeNames.every( // Make sure that none of the parent configs have a non-empty path defined
name => {
var _configs$find;
return !((_configs$find = configs.find(c => c.screen === name)) !== null && _configs$find !== void 0 && _configs$find.path);
}));
if (match) {
return createNestedStateObject(match.routeNames.map((name, i, self) => {
if (i === self.length - 1) {
return {
name,
params: parseQueryParams(path, match.parse)
};
}
return {
name
};
}), initialRoutes);
}
return undefined;
}
let result;
let current;
if (legacy === false) {
// If we're not in legacy mode,, we match the whole path against the regex instead of segments
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
const {
routes,
remainingPath
} = matchAgainstConfigs(remaining, configs.map(c => ({ ...c,
// Add `$` to the regex to make sure it matches till end of the path and not just beginning
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined
})));
if (routes !== undefined) {
// This will always be empty if full path matched
current = createNestedStateObject(routes, initialRoutes);
remaining = remainingPath;
result = current;
}
} else {
// In legacy mode, we divide the path into segments and match piece by piece
// This preserves the legacy behaviour, but we should remove it in next major
while (remaining) {
let {
routes,
remainingPath
} = matchAgainstConfigs(remaining, configs);
remaining = remainingPath; // If we hadn't matched any segments earlier, use the path as route name
if (routes === undefined) {
const segments = remaining.split('/');
routes = [{
name: decodeURIComponent(segments[0])
}];
segments.shift();
remaining = segments.join('/');
}
const state = createNestedStateObject(routes, initialRoutes);
if (current) {
var _current2;
// The state should be nested inside the deepest route we parsed before
while ((_current = current) !== null && _current !== void 0 && _current.routes[current.index || 0].state) {
var _current;
current = current.routes[current.index || 0].state;
}
current.routes[((_current2 = current) === null || _current2 === void 0 ? void 0 : _current2.index) || 0].state = state;
} else {
result = state;
}
current = state;
}
}
if (current == null || result == null) {
return undefined;
}
const route = findFocusedRoute(current);
const params = parseQueryParams(path, findParseConfigForRoute(route.name, configs));
if (params) {
// @ts-expect-error: params should be treated as read-only, but we're creating the state here so it doesn't matter
route.params = { ...route.params,
...params
};
}
return result;
}
const joinPaths = (...paths) => [].concat(...paths.map(p => p.split('/'))).filter(Boolean).join('/');
const matchAgainstConfigs = (remaining, configs) => {
let routes;
let remainingPath = remaining; // Go through all configs, and see if the next path segment matches our regex
for (const config of configs) {
if (!config.regex) {
continue;
}
const match = remainingPath.match(config.regex); // If our regex matches, we need to extract params from the path
if (match) {
var _config$pattern;
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, {
// The param segments appear every second item starting from 2 in the regex match result
[p]: match[(i + 1) * 2].replace(/\//, '')
}), {});
routes = config.routeNames.map(name => {
var _config$path;
const config = configs.find(c => c.screen === name);
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) => {
const value = matchedParams[p];
if (value) {
var _config$parse;
const key = p.replace(/^:/, '').replace(/\?$/, '');
acc[key] = (_config$parse = config.parse) !== null && _config$parse !== void 0 && _config$parse[key] ? config.parse[key](value) : value;
}
return acc;
}, {});
if (params && Object.keys(params).length) {
return {
name,
params
};
}
return {
name
};
});
remainingPath = remainingPath.replace(match[1], '');
break;
}
}
return {
routes,
remainingPath
};
};
const createNormalizedConfigs = (legacy, screen, routeConfig, routeNames = [], initials, parentPattern) => {
const configs = [];
routeNames.push(screen);
const config = routeConfig[screen];
if (typeof config === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
configs.push(createConfigItem(legacy, screen, routeNames, pattern, config));
} else if (typeof config === 'object') {
let pattern; // if an object is specified as the value (e.g. Foo: { ... }),
// it can have `path` property and
// it could have `screens` prop which has nested configs
if (typeof config.path === 'string') {
if (legacy) {
pattern = config.exact !== true && parentPattern ? joinPaths(parentPattern, config.path) : config.path;
} else {
if (config.exact && config.path === undefined) {
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: ''`.");
}
pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || '';
}
configs.push(createConfigItem(legacy, screen, routeNames, pattern, config.path, config.parse));
}
if (config.screens) {
// property `initialRouteName` without `screens` has no purpose
if (config.initialRouteName) {
initials.push({
initialRouteName: config.initialRouteName,
connectedRoutes: Object.keys(config.screens)
});
}
Object.keys(config.screens).forEach(nestedConfig => {
var _pattern;
const result = createNormalizedConfigs(legacy, nestedConfig, config.screens, routeNames, initials, (_pattern = pattern) !== null && _pattern !== void 0 ? _pattern : parentPattern);
configs.push(...result);
});
}
}
routeNames.pop();
return configs;
};
const createConfigItem = (legacy, screen, routeNames, pattern, path, parse) => {
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
pattern = pattern.split('/').filter(Boolean).join('/');
const regex = pattern ? new RegExp("^(".concat(pattern.split('/').map(it => {
if (legacy && it === '*') {
throw new Error("Please update your config to the new format to use wildcard pattern ('*'). https://reactnavigation.org/docs/configuring-links/#updating-config");
}
if (it.startsWith(':')) {
return "(([^/]+\\/)".concat(it.endsWith('?') ? '?' : '', ")");
}
return "".concat(it === '*' ? '.*' : (0, _escapeStringRegexp.default)(it), "\\/");
}).join(''), ")")) : undefined;
return {
screen,
regex,
pattern,
path,
// The routeNames array is mutated, so copy it to keep the current state
routeNames: [...routeNames],
parse
};
};
const findParseConfigForRoute = (routeName, flatConfig) => {
for (const config of flatConfig) {
if (routeName === config.routeNames[config.routeNames.length - 1]) {
return config.parse;
}
}
return undefined;
}; // Try to find an initial route connected with the one passed
const findInitialRoute = (routeName, initialRoutes) => {
for (const config of initialRoutes) {
if (config.connectedRoutes.includes(routeName)) {
return config.initialRouteName === routeName ? undefined : config.initialRouteName;
}
}
return undefined;
}; // returns state object with values depending on whether
// it is the end of state and if there is initialRoute for this level
const createStateObject = (initialRoute, route, isEmpty) => {
if (isEmpty) {
if (initialRoute) {
return {
index: 1,
routes: [{
name: initialRoute
}, route]
};
} else {
return {
routes: [route]
};
}
} else {
if (initialRoute) {
return {
index: 1,
routes: [{
name: initialRoute
}, { ...route,
state: {
routes: []
}
}]
};
} else {
return {
routes: [{ ...route,
state: {
routes: []
}
}]
};
}
}
};
const createNestedStateObject = (routes, initialRoutes) => {
let state;
let route = routes.shift();
let initialRoute = findInitialRoute(route.name, initialRoutes);
state = createStateObject(initialRoute, route, routes.length === 0);
if (routes.length > 0) {
let nestedState = state;
while (route = routes.shift()) {
initialRoute = findInitialRoute(route.name, initialRoutes);
const nestedStateIndex = nestedState.index || nestedState.routes.length - 1;
nestedState.routes[nestedStateIndex].state = createStateObject(initialRoute, route, routes.length === 0);
if (routes.length > 0) {
nestedState = nestedState.routes[nestedStateIndex].state;
}
}
}
return state;
};
const findFocusedRoute = state => {
var _current4;
let current = state;
while ((_current3 = current) !== null && _current3 !== void 0 && _current3.routes[current.index || 0].state) {
var _current3;
// The query params apply to the deepest route
current = current.routes[current.index || 0].state;
}
const route = current.routes[((_current4 = current) === null || _current4 === void 0 ? void 0 : _current4.index) || 0];
return route;
};
const parseQueryParams = (path, parseConfig) => {
const query = path.split('?')[1];
const params = queryString.parse(query);
if (parseConfig) {
Object.keys(params).forEach(name => {
if (parseConfig[name] && typeof params[name] === 'string') {
params[name] = parseConfig[name](params[name]);
}
});
}
return Object.keys(params).length ? params : undefined;
};
//# sourceMappingURL=getStateFromPath.js.map
;