diff --git a/example/src/Screens/NotFound.tsx b/example/src/Screens/NotFound.tsx new file mode 100644 index 00000000..3641e829 --- /dev/null +++ b/example/src/Screens/NotFound.tsx @@ -0,0 +1,40 @@ +import { StackNavigationProp } from '@react-navigation/stack'; +import * as React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { Button } from 'react-native-paper'; + +const NotFoundScreen = ({ + navigation, +}: { + navigation: StackNavigationProp<{ Home: undefined }>; +}) => { + return ( + + 404 Not Found + + + ); +}; + +export default NotFoundScreen; + +const styles = StyleSheet.create({ + title: { + fontSize: 36, + }, + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 8, + }, + button: { + margin: 24, + }, +}); diff --git a/example/src/index.tsx b/example/src/index.tsx index 46ed9b2c..78ff5ec3 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -48,6 +48,7 @@ import StackHeaderCustomization from './Screens/StackHeaderCustomization'; import BottomTabs from './Screens/BottomTabs'; import MaterialTopTabsScreen from './Screens/MaterialTopTabs'; import MaterialBottomTabs from './Screens/MaterialBottomTabs'; +import NotFound from './Screens/NotFound'; import DynamicTabs from './Screens/DynamicTabs'; import AuthFlow from './Screens/AuthFlow'; import CompatAPI from './Screens/CompatAPI'; @@ -68,6 +69,7 @@ type RootDrawerParamList = { type RootStackParamList = { Home: undefined; + NotFound: undefined; } & { [P in keyof typeof SCREENS]: undefined; }; @@ -231,7 +233,10 @@ export default function App() { return acc; }, - { Home: '' } + { + Home: '', + NotFound: '*', + } ), }, Article: { @@ -332,6 +337,11 @@ export default function App() { )} + {(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map( (name) => ( { state ); }); + +it('matches wildcard patterns at root', () => { + const path = '/test/bar/42/whatever'; + const config = { + 404: '*', + Foo: { + screens: { + Bar: { + path: '/bar/:id/', + }, + }, + }, + }; + + const state = { + routes: [{ name: '404' }], + }; + + expect(getStateFromPath(path, config)).toEqual(state); + expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( + state + ); +}); + +it('matches wildcard patterns at nested level', () => { + const path = '/bar/42/whatever/baz/initt'; + const config = { + Foo: { + screens: { + Bar: { + path: '/bar/:id/', + screens: { + 404: '*', + }, + }, + }, + }, + }; + + const state = { + routes: [ + { + name: 'Foo', + state: { + routes: [ + { + name: 'Bar', + params: { id: '42' }, + state: { + routes: [{ name: '404' }], + }, + }, + ], + }, + }, + ], + }; + + expect(getStateFromPath(path, config)).toEqual(state); + expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( + state + ); +}); + +it('matches wildcard patterns at nested level with exact', () => { + const path = '/whatever'; + const config = { + Foo: { + screens: { + Bar: { + path: '/bar/:id/', + screens: { + 404: { + path: '*', + exact: true, + }, + }, + }, + Baz: {}, + }, + }, + }; + + const state = { + routes: [ + { + name: 'Foo', + state: { + routes: [ + { + name: 'Bar', + state: { + routes: [{ name: '404' }], + }, + }, + ], + }, + }, + ], + }; + + expect(getStateFromPath(path, config)).toEqual(state); + expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( + state + ); +}); + +it('tries to match wildcard patterns at the end', () => { + const path = '/bar/42/test'; + const config = { + Foo: { + screens: { + Bar: { + path: '/bar/:id/', + screens: { + 404: '*', + Test: 'test', + }, + }, + }, + }, + }; + + const state = { + routes: [ + { + name: 'Foo', + state: { + routes: [ + { + name: 'Bar', + params: { id: '42' }, + state: { + routes: [{ name: 'Test' }], + }, + }, + ], + }, + }, + ], + }; + + expect(getStateFromPath(path, config)).toEqual(state); + expect(getStateFromPath(getPathFromState(state, config), config)).toEqual( + state + ); +}); diff --git a/packages/core/src/getStateFromPath.tsx b/packages/core/src/getStateFromPath.tsx index 0a550759..f9c33e69 100644 --- a/packages/core/src/getStateFromPath.tsx +++ b/packages/core/src/getStateFromPath.tsx @@ -59,11 +59,46 @@ export default function getStateFromPath( createNormalizedConfigs(key, options, [], initialRoutes) ) ) - .sort( - (a, b) => - // Sort configs so the most exhaustive is always first to be chosen - b.pattern.split('/').length - a.pattern.split('/').length - ); + .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 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; + }); let remaining = path .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones @@ -311,7 +346,7 @@ const createConfigItem = ( return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`; } - return `${escape(it)}\\/`; + return `${it === '*' ? '.*' : escape(it)}\\/`; }) .join('')})` )