diff --git a/.changeset/url-template-parser.md b/.changeset/url-template-parser.md new file mode 100644 index 00000000000..cbdbc4fcce7 --- /dev/null +++ b/.changeset/url-template-parser.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': minor +'@clerk/ui': patch +--- + +Add `createDynamicParamParser` and `populateParamFromObject` to `@clerk/shared/url` for resolving `:property` placeholders in URL templates. diff --git a/packages/shared/src/__tests__/url.spec.ts b/packages/shared/src/__tests__/url.spec.ts index 4fcf79b3d0a..09f86354235 100644 --- a/packages/shared/src/__tests__/url.spec.ts +++ b/packages/shared/src/__tests__/url.spec.ts @@ -3,6 +3,7 @@ import { describe, expect, it, test } from 'vitest'; import { addClerkPrefix, cleanDoubleSlashes, + createDynamicParamParser, getClerkJsMajorVersionOrTag, getScriptUrl, joinURL, @@ -252,3 +253,29 @@ describe('cleanDoubleSlashes', () => { expect(cleanDoubleSlashes()).toBe(''); }); }); + +describe('createDynamicParamParser', () => { + const entity = { + foo: 'foo_string', + bar: 'bar_string', + }; + + const testCases = [ + [':foo', entity, 'foo_string'], + ['/:foo', entity, '/foo_string'], + ['/some/:bar/any', entity, '/some/bar_string/any'], + ['/:notValid', entity, '/:notValid'], + ] as const; + + it.each(testCases)( + 'replaces the dynamic param with the value assigned to the key inside the object. Url=(%s), Object=(%s), result=(%s)', + (urlWithParam, obj, result) => { + expect( + createDynamicParamParser({ regex: /:(\w+)/ })({ + urlWithParam, + entity: obj, + }), + ).toEqual(result); + }, + ); +}); diff --git a/packages/shared/src/url.ts b/packages/shared/src/url.ts index bef7055749d..0097b467654 100644 --- a/packages/shared/src/url.ts +++ b/packages/shared/src/url.ts @@ -219,3 +219,20 @@ export function joinURL(base: string, ...input: string[]): string { // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/; export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url); + +export const createDynamicParamParser = + ({ regex }: { regex: RegExp }) => + >({ urlWithParam, entity }: { urlWithParam: string; entity: T }) => { + const match = regex.exec(urlWithParam); + + if (match) { + const key = match[1]; + if (key in entity) { + const value = entity[key] as string; + return urlWithParam.replace(match[0], value); + } + } + return urlWithParam; + }; + +export const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ }); diff --git a/packages/ui/src/contexts/utils.ts b/packages/ui/src/contexts/utils.ts index 260c8965dbe..7b279b2b159 100644 --- a/packages/ui/src/contexts/utils.ts +++ b/packages/ui/src/contexts/utils.ts @@ -1,7 +1,7 @@ import { clerkCoreErrorContextProviderNotFound } from '@clerk/shared/internal/clerk-js/errors'; import { snakeToCamel } from '@clerk/shared/underscore'; -import { createDynamicParamParser } from '../utils/dynamicParamParser'; +export { populateParamFromObject } from '@clerk/shared/url'; export function assertContextExists(contextVal: unknown, providerName: string): asserts contextVal { if (!contextVal) { @@ -20,5 +20,3 @@ export function getInitialValuesFromQueryParams(queryString: string, params: str return props; } - -export const populateParamFromObject = createDynamicParamParser({ regex: /:(\w+)/ }); diff --git a/packages/ui/src/mosaic/features/user-button/user-button.model.tsx b/packages/ui/src/mosaic/features/user-button/user-button.model.tsx index 603ec84afa6..6f878218ecf 100644 --- a/packages/ui/src/mosaic/features/user-button/user-button.model.tsx +++ b/packages/ui/src/mosaic/features/user-button/user-button.model.tsx @@ -9,8 +9,8 @@ import type { UserProfileModalProps, UserResource, } from '@clerk/shared/types'; +import { populateParamFromObject } from '@clerk/shared/url'; -import { populateParamFromObject } from '../../../contexts/utils'; import { useOrganizationListInView } from '../../../hooks/useOrganizationListInView'; import { useMosaicEnvironment } from '../../hooks/useMosaicEnvironment'; import { useMosaicRouter } from '../../hooks/useMosaicRouter'; diff --git a/packages/ui/src/utils/__tests__/dynamicParamParser.test.ts b/packages/ui/src/utils/__tests__/dynamicParamParser.test.ts deleted file mode 100644 index 712de7756a4..00000000000 --- a/packages/ui/src/utils/__tests__/dynamicParamParser.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { createDynamicParamParser } from '../dynamicParamParser'; - -const entity = { - foo: 'foo_string', - bar: 'bar_string', -}; - -describe('createDynamicParamParser', () => { - const testCases = [ - [':foo', entity, 'foo_string'], - ['/:foo', entity, '/foo_string'], - ['/some/:bar/any', entity, '/some/bar_string/any'], - ['/:notValid', entity, '/:notValid'], - ] as const; - - it.each(testCases)( - 'replaces the dynamic param with the value assigned to the key inside the object. Url=(%s), Object=(%s), result=(%s)', - (urlWithParam, obj, result) => { - expect( - createDynamicParamParser({ regex: /:(\w+)/ })({ - urlWithParam, - entity: obj, - }), - ).toEqual(result); - }, - ); -}); diff --git a/packages/ui/src/utils/dynamicParamParser.ts b/packages/ui/src/utils/dynamicParamParser.ts deleted file mode 100644 index 9f254312d9e..00000000000 --- a/packages/ui/src/utils/dynamicParamParser.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const createDynamicParamParser = - ({ regex }: { regex: RegExp }) => - >({ urlWithParam, entity }: { urlWithParam: string; entity: T }) => { - const match = regex.exec(urlWithParam); - - if (match) { - const key = match[1]; - if (key in entity) { - const value = entity[key] as string; - return urlWithParam.replace(match[0], value); - } - } - return urlWithParam; - };