From cc5a474bf30ecbfabf264612a90916cd48f104f7 Mon Sep 17 00:00:00 2001 From: Damien Simonin Feugas Date: Fri, 24 May 2024 11:38:10 +0200 Subject: [PATCH] fix: nextjs parallel routes with catchall isn't supported (#141) --- packages/web/package.json | 2 +- packages/web/src/utils.test.ts | 22 +++++++++++++++++++++- packages/web/src/utils.ts | 31 +++++++++++++++++++++---------- 3 files changed, 43 insertions(+), 12 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..d2727d4 100644 --- a/packages/web/src/utils.ts +++ b/packages/web/src/utils.ts @@ -83,25 +83,36 @@ 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)}(?=[/?#]|$)`); - if (matcher.test(result)) { - result = result.replace(matcher, `/[${expr}]`); + const entries = Object.entries(pathParams); + // simple keys must be handled first + for (const [key, value] of entries) { + if (!Array.isArray(value)) { + const matcher = turnValueToRegExp(value); + if (matcher.test(result)) { + result = result.replace(matcher, `/[${key}]`); + } + } + } + // array values next + for (const [key, value] of entries) { + if (Array.isArray(value)) { + const matcher = turnValueToRegExp(value.join('/')); + if (matcher.test(result)) { + result = result.replace(matcher, `/[...${key}]`); + } } } - return result; } catch (e) { return pathname; } } +function turnValueToRegExp(value: string): RegExp { + return new RegExp(`/${escapeRegExp(value)}(?=[/?#]|$)`); +} + function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }