From 3a04c04c4bf7a3da10f57a7dcb14364152d40e8c Mon Sep 17 00:00:00 2001 From: feugy Date: Thu, 23 May 2024 15:05:00 +0200 Subject: [PATCH] fix: nextjs parallel routes with catchall isn't supported --- packages/web/package.json | 2 +- packages/web/src/utils.test.ts | 22 ++++++++++++++++++++- packages/web/src/utils.ts | 35 ++++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/web/package.json b/packages/web/package.json index 510d49e..83021b4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@vercel/analytics", - "version": "1.3.0", + "version": "1.3.1", "description": "Gain real-time traffic insights with Vercel Web Analytics", "keywords": [ "analytics", diff --git a/packages/web/src/utils.test.ts b/packages/web/src/utils.test.ts index c132bc1..c864c65 100644 --- a/packages/web/src/utils.test.ts +++ b/packages/web/src/utils.test.ts @@ -145,12 +145,21 @@ describe('utils', () => { const input = '/en/us/next-site'; const params = { langs: ['en', 'us'], - teamSlug: 'vercel', }; const expected = '/[...langs]/next-site'; expect(computeRoute(input, params)).toBe(expected); }); + it('handles array segments and individual segments', () => { + const input = '/en/us/next-site'; + const params = { + langs: ['en', 'us'], + team: 'next-site', + }; + const expected = '/[...langs]/[team]'; + expect(computeRoute(input, params)).toBe(expected); + }); + it('handles special characters in url', () => { const input = '/123/test(test'; const params = { @@ -172,6 +181,17 @@ describe('utils', () => { expect(computeRoute(input, params)).toBe(expected); }); + it('parallel routes where params matched both individually and within arrays', () => { + const params = { + catchAll: ['m', 'john', 'p', 'shirt'], + merchantId: 'john', + productSlug: 'shirt', + }; + expect(computeRoute('/m/john/p/shirt', params)).toBe( + '/m/[merchantId]/p/[productSlug]' + ); + }); + describe('edge case handling (same values for multiple params)', () => { it('replaces based on the priority of the pathParams keys', () => { const input = '/test/test'; diff --git a/packages/web/src/utils.ts b/packages/web/src/utils.ts index e0d4ccb..fa12fcb 100644 --- a/packages/web/src/utils.ts +++ b/packages/web/src/utils.ts @@ -83,16 +83,31 @@ export function computeRoute( } let result = pathname; - try { - for (const [key, valueOrArray] of Object.entries(pathParams)) { - const isValueArray = Array.isArray(valueOrArray); - const value = isValueArray ? valueOrArray.join('/') : valueOrArray; - const expr = isValueArray ? `...${key}` : key; - - const matcher = new RegExp(`/${escapeRegExp(value)}(?=[/?#]|$)`); + const keys = Object.entries(pathParams).reduce<{ + simple: string[]; + multiple: string[]; + }>( + ({ simple, multiple }, [key, value]) => ({ + simple: Array.isArray(value) ? simple : [...simple, key], + multiple: Array.isArray(value) ? [...multiple, key] : multiple, + }), + { simple: [], multiple: [] } + ); + // simple keys must be handled first + for (const key of keys.simple) { + const matcher = turnValueToRegExp(pathParams[key] as string); + if (matcher.test(result)) { + result = result.replace(matcher, `/[${key}]`); + } + } + // array values next + for (const key of keys.multiple) { + const matcher = turnValueToRegExp( + (pathParams[key] as string[]).join('/') + ); if (matcher.test(result)) { - result = result.replace(matcher, `/[${expr}]`); + result = result.replace(matcher, `/[...${key}]`); } } @@ -102,6 +117,10 @@ export function computeRoute( } } +function turnValueToRegExp(value: string): RegExp { + return new RegExp(`/${escapeRegExp(value)}(?=[/?#]|$)`); +} + function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }