fix: drop support for legacy linking config

BREAKING CHANGE: This commit drops support for legacy linking config which allowed screens to be specified without the screens property in the config.
This commit is contained in:
Satyajit Sahoo
2020-11-11 23:33:18 +01:00
parent 366d0181dc
commit 0e13e8d23c
5 changed files with 43 additions and 1736 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -98,62 +98,6 @@ it('converts path string to initial state with config', () => {
);
});
it('converts path string to initial state with config (legacy)', () => {
const path = '/foo/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
const config = {
Foo: 'foo',
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) => author.toLowerCase(),
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
// @ts-expect-error: legacy config
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('handles leading slash when converting', () => {
expect(getStateFromPath('/foo/bar/?count=42')).toEqual({
routes: [
@@ -284,77 +228,6 @@ it('converts path string to initial state with config with nested screens', () =
);
});
it('converts path string to initial state with config with nested screens (legacy)', () => {
const path = '/foe/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) => author.toLowerCase(),
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
// @ts-expect-error: legacy config
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('converts path string to initial state with config with nested screens and unused parse functions', () => {
const path = '/foe/baz/jane?count=10&answer=42&valid=true';
const config = {
@@ -417,66 +290,6 @@ it('converts path string to initial state with config with nested screens and un
);
});
it('converts path string to initial state with config with nested screens and unused parse functions (legacy)', () => {
const path = '/foe/baz/jane?count=10&answer=42&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
id: Boolean,
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
// @ts-expect-error: legacy config
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('handles nested object with unused configs and with parse in it', () => {
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
const config = {
@@ -568,95 +381,6 @@ it('handles nested object with unused configs and with parse in it', () => {
);
});
it('handles nested object with unused configs and with parse in it (legacy)', () => {
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: {
path: 'bos',
exact: true,
},
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
// @ts-expect-error: legacy config
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('handles parse in nested object for second route depth', () => {
const path = '/baz';
const config = {
@@ -984,97 +708,6 @@ it('handles two initialRouteNames', () => {
);
});
it('handles two initialRouteNames (legacy)', () => {
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
initialRouteName: 'Bos',
screens: {
Bos: {
path: 'bos',
exact: true,
},
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
state: {
index: 1,
routes: [
{ name: 'Bos' },
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
// @ts-expect-error: legacy config
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('accepts initialRouteName without config for it', () => {
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
const config = {
@@ -1169,97 +802,6 @@ it('accepts initialRouteName without config for it', () => {
);
});
it('accepts initialRouteName without config for it (legacy)', () => {
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
initialRouteName: 'Bas',
screens: {
Bos: {
path: 'bos',
exact: true,
},
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
state: {
index: 1,
routes: [
{ name: 'Bas' },
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
// @ts-expect-error: legacy config
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('returns undefined if path is empty and no matching screen is present', () => {
const config = {
screens: {
@@ -2713,111 +2255,3 @@ it('throws if two screens map to the same pattern', () => {
})
).not.toThrow();
});
it('throws if wildcard is specified with legacy config', () => {
const path = '/bar/42/baz/test';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: 'baz',
},
},
404: '*',
},
},
};
// @ts-expect-error: legacy config
expect(() => getStateFromPath(path, config)).toThrow(
"Please update your config to the new format to use wildcard pattern ('*')"
);
});
it('supports legacy config', () => {
const path = '/foo/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
const config = {
Foo: 'foo',
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) => author.toLowerCase(),
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
};
// @ts-expect-error: legacy config
expect(getStateFromPath(path, config)).toEqual(state);
});
it("throws when using 'initialRouteName' or 'screens' with legacy config", () => {
expect(() =>
getStateFromPath('/whatever', {
initialRouteName: 'foo',
// @ts-expect-error: legacy config
Foo: 'foo',
Bar: 'bar/:type/:fruit',
})
).toThrow('Found invalid keys in the configuration object.');
expect(() =>
getStateFromPath('/whatever', {
screens: {
Test: 'test',
},
// @ts-expect-error: legacy config
Foo: 'foo',
Bar: 'bar/:type/:fruit',
})
).toThrow('Found invalid keys in the configuration object.');
expect(() =>
getStateFromPath('/whatever', {
initialRouteName: 'foo',
screens: {
Test: 'test',
},
// @ts-expect-error: legacy config
Foo: 'foo',
Bar: 'bar/:type/:fruit',
})
).toThrow('Found invalid keys in the configuration object.');
});

View File

@@ -1,36 +0,0 @@
import type { PathConfigMap } from './types';
type Options = {
initialRouteName?: string;
screens: PathConfigMap;
};
export default function checkLegacyPathConfig(
config?: Options
): [boolean, Options | undefined] {
let legacy = false;
if (config) {
// Assume legacy configuration if config has any other keys except `screens` and `initialRouteName`
legacy = Object.keys(config).some(
(key) => key !== 'screens' && key !== 'initialRouteName'
);
if (
legacy &&
(config.hasOwnProperty('screens') ||
config.hasOwnProperty('initialRouteName'))
) {
throw new Error(
'Found invalid keys in the configuration object. See https://reactnavigation.org/docs/configuring-links/ for more details on the shape of the configuration object.'
);
}
}
if (legacy) {
// @ts-expect-error: we have incorrect type for config since we don't type legacy config
return [legacy, { screens: config }];
}
return [legacy, config];
}

View File

@@ -4,7 +4,6 @@ import type {
PartialState,
Route,
} from '@react-navigation/routers';
import checkLegacyPathConfig from './checkLegacyPathConfig';
import type { PathConfig, PathConfigMap } from './types';
type Options = { initialRouteName?: string; screens: PathConfigMap };
@@ -71,11 +70,9 @@ export default function getPathFromState(
);
}
const [legacy, compatOptions] = checkLegacyPathConfig(options);
// Create a normalized configs object which will be easier to use
const configs: Record<string, ConfigItem> = compatOptions
? createNormalizedConfigs(legacy, compatOptions.screens)
const configs: Record<string, ConfigItem> = options?.screens
? createNormalizedConfigs(options?.screens)
: {};
let path = '/';
@@ -177,12 +174,6 @@ export default function getPathFromState(
// Showing the route name seems ok, though whatever we show here will be incorrect
// Since the page doesn't actually exist
if (p === '*') {
if (legacy) {
throw new Error(
"Please update your config to the new format to use wildcard pattern ('*'). https://reactnavigation.org/docs/configuring-links/#updating-config"
);
}
return route.name;
}
@@ -257,7 +248,6 @@ const joinPaths = (...paths: string[]): string =>
.join('/');
const createConfigItem = (
legacy: boolean,
config: PathConfig | string,
parentPattern?: string
): ConfigItem => {
@@ -272,26 +262,19 @@ const createConfigItem = (
// It can have `path` property and `screens` prop which has nested configs
let pattern: string | undefined;
if (legacy) {
pattern =
config.exact !== true && parentPattern && config.path
? 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 || '';
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 || '';
const screens = config.screens
? createNormalizedConfigs(legacy, config.screens, pattern)
? createNormalizedConfigs(config.screens, pattern)
: undefined;
return {
@@ -303,13 +286,12 @@ const createConfigItem = (
};
const createNormalizedConfigs = (
legacy: boolean,
options: PathConfigMap,
pattern?: string
): Record<string, ConfigItem> =>
fromEntries(
Object.entries(options).map(([name, c]) => {
const result = createConfigItem(legacy, c, pattern);
const result = createConfigItem(c, pattern);
return [name, result];
})

View File

@@ -5,7 +5,6 @@ import type {
PartialState,
InitialState,
} from '@react-navigation/routers';
import checkLegacyPathConfig from './checkLegacyPathConfig';
import type { PathConfigMap } from './types';
type Options = {
@@ -63,18 +62,16 @@ export default function getStateFromPath(
path: string,
options?: Options
): ResultState | undefined {
const [legacy, compatOptions] = checkLegacyPathConfig(options);
let initialRoutes: InitialRouteConfig[] = [];
if (compatOptions?.initialRouteName) {
if (options?.initialRouteName) {
initialRoutes.push({
initialRouteName: compatOptions.initialRouteName,
connectedRoutes: Object.keys(compatOptions.screens),
initialRouteName: options.initialRouteName,
connectedRoutes: Object.keys(options.screens),
});
}
const screens = compatOptions?.screens;
const screens = options?.screens;
let remaining = path
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
@@ -111,7 +108,6 @@ export default function getStateFromPath(
.concat(
...Object.keys(screens).map((key) =>
createNormalizedConfigs(
legacy,
key,
screens as PathConfigMap,
[],
@@ -226,58 +222,22 @@ export default function getStateFromPath(
let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined;
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,
}))
);
// 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) {
// The state should be nested inside the deepest route we parsed before
while (current?.routes[current.index || 0].state) {
current = current.routes[current.index || 0].state;
}
(current as PartialState<NavigationState>).routes[
current?.index || 0
].state = state;
} else {
result = state;
}
current = state;
}
if (routes !== undefined) {
// This will always be empty if full path matched
current = createNestedStateObject(routes, initialRoutes);
remaining = remainingPath;
result = current;
}
if (current == null || result == null) {
@@ -363,7 +323,6 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
};
const createNormalizedConfigs = (
legacy: boolean,
screen: string,
routeConfig: PathConfigMap,
routeNames: string[] = [],
@@ -380,7 +339,7 @@ const createNormalizedConfigs = (
// 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));
configs.push(createConfigItem(screen, routeNames, pattern, config));
} else if (typeof config === 'object') {
let pattern: string | undefined;
@@ -388,33 +347,19 @@ const createNormalizedConfigs = (
// 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 || '';
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
)
createConfigItem(screen, routeNames, pattern, config.path, config.parse)
);
}
@@ -429,7 +374,6 @@ const createNormalizedConfigs = (
Object.keys(config.screens).forEach((nestedConfig) => {
const result = createNormalizedConfigs(
legacy,
nestedConfig,
config.screens as PathConfigMap,
routeNames,
@@ -448,7 +392,6 @@ const createNormalizedConfigs = (
};
const createConfigItem = (
legacy: boolean,
screen: string,
routeNames: string[],
pattern: string,
@@ -463,12 +406,6 @@ const createConfigItem = (
`^(${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 `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
}