diff --git a/packages/core/src/__tests__/getStateFromPath.test.tsx b/packages/core/src/__tests__/getStateFromPath.test.tsx index 02f87d20..27ec3eb5 100644 --- a/packages/core/src/__tests__/getStateFromPath.test.tsx +++ b/packages/core/src/__tests__/getStateFromPath.test.tsx @@ -2673,6 +2673,47 @@ it('uses nearest parent wildcard match for unmatched paths', () => { ); }); +it('throws if two screens map to the same pattern', () => { + const path = '/bar/42/baz/test'; + + expect(() => + getStateFromPath(path, { + screens: { + Foo: { + screens: { + Bar: { + path: '/bar/:id/', + screens: { + Baz: 'baz', + }, + }, + Bax: '/bar/:id/baz', + }, + }, + }, + }) + ).toThrow( + "Found conflicting screens with the same pattern. The pattern 'bar/:id/baz' resolves to both 'Foo > Bax' and 'Foo > Bar > Baz'. Patterns must be unique and cannot resolve to more than one screen." + ); + + expect(() => + getStateFromPath(path, { + screens: { + Foo: { + screens: { + Bar: { + path: '/bar/:id/', + screens: { + Baz: '', + }, + }, + }, + }, + }, + }) + ).not.toThrow(); +}); + it('throws if wildcard is specified with legacy config', () => { const path = '/bar/42/baz/test'; const config = { diff --git a/packages/core/src/getPathFromState.tsx b/packages/core/src/getPathFromState.tsx index 4cb2bf0c..ba81f5c0 100644 --- a/packages/core/src/getPathFromState.tsx +++ b/packages/core/src/getPathFromState.tsx @@ -239,6 +239,10 @@ export default function getPathFromState( // Object.fromEntries is not available in older iOS versions const fromEntries = (entries: (readonly [K, V])[]) => entries.reduce((acc, [k, v]) => { + if (acc.hasOwnProperty(k)) { + throw new Error(`A value for key '${k}' already exists in the object.`); + } + acc[k] = v; return acc; }, {} as Record); diff --git a/packages/core/src/getStateFromPath.tsx b/packages/core/src/getStateFromPath.tsx index 77696f61..73dd16ce 100644 --- a/packages/core/src/getStateFromPath.tsx +++ b/packages/core/src/getStateFromPath.tsx @@ -119,6 +119,12 @@ export default function getStateFromPath( // - 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)) { @@ -155,6 +161,35 @@ export default function getStateFromPath( 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 '${ + config.pattern + }' resolves to both '${a.join(' > ')}' and '${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