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('')})`
)