From 5a9e34ebd8371a9b8112bb04fb3d1891bf85f4ad Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Wed, 28 Aug 2024 13:36:41 -0500 Subject: [PATCH 1/4] Move elements router into shared --- packages/shared/src/router.ts | 3 + .../src/router/__tests__/router.test.ts | 129 +++++++++++++++ packages/shared/src/router/react.tsx | 55 +++++++ packages/shared/src/router/router.ts | 150 ++++++++++++++++++ packages/shared/src/router/types.ts | 1 + packages/shared/src/url.ts | 7 + packages/shared/subpaths.mjs | 1 + 7 files changed, 346 insertions(+) create mode 100644 packages/shared/src/router.ts create mode 100644 packages/shared/src/router/__tests__/router.test.ts create mode 100644 packages/shared/src/router/react.tsx create mode 100644 packages/shared/src/router/router.ts create mode 100644 packages/shared/src/router/types.ts diff --git a/packages/shared/src/router.ts b/packages/shared/src/router.ts new file mode 100644 index 00000000000..a6bc0ca6a83 --- /dev/null +++ b/packages/shared/src/router.ts @@ -0,0 +1,3 @@ +export { type ClerkRouter, type ClerkHostRouter, createClerkRouter } from './router/router'; +export { type RoutingMode } from './router/types'; +export { Router, useClerkRouter, Route, ClerkRouterContext } from './router/react'; diff --git a/packages/shared/src/router/__tests__/router.test.ts b/packages/shared/src/router/__tests__/router.test.ts new file mode 100644 index 00000000000..df1e5a3f497 --- /dev/null +++ b/packages/shared/src/router/__tests__/router.test.ts @@ -0,0 +1,129 @@ +import { createClerkRouter } from '../router'; + +describe('createClerkRouter', () => { + const mockRouter = { + name: 'mockRouter', + mode: 'path' as const, + pathname: jest.fn(), + searchParams: jest.fn(), + push: jest.fn(), + shallowPush: jest.fn(), + replace: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a ClerkRouter instance with the correct base path', () => { + const oneBasePath = '/app'; + const twoBasePath = 'app'; + const threeBasePath = 'app/'; + const one = createClerkRouter(mockRouter, oneBasePath); + const two = createClerkRouter(mockRouter, twoBasePath); + const three = createClerkRouter(mockRouter, threeBasePath); + + expect(one.basePath).toBe(oneBasePath); + expect(two.basePath).toBe('/app'); + expect(three.basePath).toBe('/app'); + }); + + it('matches the path correctly', () => { + const path = '/dashboard'; + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.pathname.mockReturnValue('/app/dashboard'); + + expect(clerkRouter.match(path)).toBe(true); + }); + + it('normalizes path arguments internally', () => { + const path = 'dashboard/'; + const clerkRouter = createClerkRouter(mockRouter, 'app/'); + + mockRouter.pathname.mockReturnValue('/app/dashboard'); + + expect(clerkRouter.match(path)).toBe(true); + }); + + it('throws an error when no path is provided', () => { + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + expect(() => { + clerkRouter.match(); + }).toThrow('[clerk] router.match() requires either a path to match, or the index flag must be set to true.'); + }); + + it('creates a child router with the correct base path', () => { + const clerkRouter = createClerkRouter(mockRouter, '/app'); + const childRouter = clerkRouter.child('dashboard'); + + expect(childRouter.basePath).toBe('/app/dashboard'); + }); + + it('pushes the correct destination URL ', () => { + const path = '/app/dashboard'; + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.searchParams.mockImplementation(() => new URLSearchParams('')); + clerkRouter.push(path); + + expect(mockRouter.push).toHaveBeenCalledWith('/app/dashboard'); + }); + + it('replaces the correct destination URL', () => { + const path = '/app/dashboard'; + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.searchParams.mockImplementation(() => new URLSearchParams('')); + clerkRouter.replace(path); + + expect(mockRouter.replace).toHaveBeenCalledWith('/app/dashboard'); + }); + + it('pushes the correct destination URL with preserved query parameters', () => { + const path = '/app/dashboard'; + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar')); + clerkRouter.push(path); + + expect(mockRouter.push).toHaveBeenCalledWith('/app/dashboard?after_sign_in_url=foobar'); + }); + + it('replaces the correct destination URL with preserved query parameters', () => { + const path = '/app/dashboard'; + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar')); + clerkRouter.replace(path); + + expect(mockRouter.replace).toHaveBeenCalledWith('/app/dashboard?after_sign_in_url=foobar'); + }); + + it('pushes absolute URLs unmodified', () => { + const path = 'https://example.com'; + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar')); + clerkRouter.push(path); + + expect(mockRouter.push).toHaveBeenCalledWith('https://example.com'); + }); + + it('returns the correct pathname', () => { + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.pathname.mockReturnValue('/app/dashboard'); + + expect(clerkRouter.pathname()).toBe('/app/dashboard'); + }); + + it('returns the correct searchParams', () => { + const clerkRouter = createClerkRouter(mockRouter, '/app'); + + mockRouter.searchParams.mockImplementation(() => new URLSearchParams('foo=bar')); + + expect(clerkRouter.searchParams().get('foo')).toEqual('bar'); + }); +}); diff --git a/packages/shared/src/router/react.tsx b/packages/shared/src/router/react.tsx new file mode 100644 index 00000000000..c0fcce0649b --- /dev/null +++ b/packages/shared/src/router/react.tsx @@ -0,0 +1,55 @@ +/** + * React-specific binding's for interacting with Clerk's router interface. + */ +import React, { createContext, useContext } from 'react'; + +import type { ClerkHostRouter, ClerkRouter } from './router'; +import { createClerkRouter } from './router'; + +export const ClerkRouterContext = createContext(null); + +export function useClerkRouter() { + const ctx = useContext(ClerkRouterContext); + + if (!ctx) { + throw new Error('clerk: Unable to locate ClerkRouter, make sure this is rendered within ``.'); + } + + return ctx; +} + +/** + * Construct a Clerk Router using the provided host router. The router instance is accessible using `useClerkRouter()`. + */ +export function Router({ + basePath, + children, + router, +}: { + children: React.ReactNode; + basePath?: string; + router: ClerkHostRouter; +}) { + const clerkRouter = createClerkRouter(router, basePath); + + return {children}; +} + +type RouteProps = { path?: string; index?: boolean }; + +/** + * Used to conditionally render its children based on whether or not the current path matches the provided path. + */ +export function Route({ path, children, index }: RouteProps & { children: React.ReactNode }) { + const parentRouter = useClerkRouter(); + + if (!path && !index) { + return children; + } + + if (!parentRouter?.match(path, index)) { + return null; + } + + return children; +} diff --git a/packages/shared/src/router/router.ts b/packages/shared/src/router/router.ts new file mode 100644 index 00000000000..d20e6760b3b --- /dev/null +++ b/packages/shared/src/router/router.ts @@ -0,0 +1,150 @@ +import { isAbsoluteUrl, withLeadingSlash, withoutTrailingSlash } from '../url'; +import type { RoutingMode } from './types'; + +export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up_url', 'redirect_url']; + +/** + * This type represents a generic router interface that Clerk relies on to interact with the host router. + */ +export type ClerkHostRouter = { + readonly mode: RoutingMode; + readonly name: string; + pathname: () => string; + push: (path: string) => void; + replace: (path: string) => void; + searchParams: () => URLSearchParams; + shallowPush: (path: string) => void; +}; + +/** + * Internal Clerk router, used by Clerk components to interact with the host's router. + */ +export type ClerkRouter = { + /** + * The basePath the router is currently mounted on. + */ + basePath: string; + /** + * Creates a child router instance scoped to the provided base path. + */ + child: (childBasePath: string) => ClerkRouter; + /** + * Matches the provided path against the router's current path. If index is provided, matches against the root route of the router. + */ + match: (path?: string, index?: boolean) => boolean; + + /** + * Mode of the router instance, path-based or virtual + */ + readonly mode: RoutingMode; + + /** + * Name of the router instance + */ + readonly name: string; + + /** + * Navigates to the provided path via a history push + */ + push: ClerkHostRouter['push']; + /** + * Navigates to the provided path via a history replace + */ + replace: ClerkHostRouter['replace']; + /** + * If supported by the host router, navigates to the provided path without triggering a full navigation + */ + shallowPush: ClerkHostRouter['shallowPush']; + /** + * Returns the current pathname (including the base path) + */ + pathname: ClerkHostRouter['pathname']; + /** + * Returns the current search params + */ + searchParams: ClerkHostRouter['searchParams']; +}; + +/** + * Ensures the provided path has a leading slash and no trailing slash + */ +function normalizePath(path: string) { + return withoutTrailingSlash(withLeadingSlash(path)); +} + +/** + * Factory function to create an instance of ClerkRouter with the provided host router. + * + * @param router host router instance to be used by the router + * @param basePath base path of the router, navigation and matching will be scoped to this path + * @returns A ClerkRouter instance + */ +export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/'): ClerkRouter { + const normalizedBasePath = normalizePath(basePath); + + /** + * Certain query parameters need to be preserved when navigating internally. These query parameters are ultimately used by Clerk to dictate behavior, so we keep them around. + */ + function makeDestinationUrlWithPreservedQueryParameters(path: string) { + // If the provided path is an absolute URL, return it unmodified. + if (isAbsoluteUrl(path)) { + return path; + } + + const destinationUrl = new URL(path, window.location.origin); + const currentSearchParams = router.searchParams(); + + PRESERVED_QUERYSTRING_PARAMS.forEach(key => { + const maybeValue = currentSearchParams.get(key); + if (maybeValue) { + destinationUrl.searchParams.set(key, maybeValue); + } + }); + + return `${destinationUrl.pathname}${destinationUrl.search}`; + } + + function match(path?: string, index?: boolean) { + const pathToMatch = path ?? (index && '/'); + + if (!pathToMatch) { + throw new Error('[clerk] router.match() requires either a path to match, or the index flag must be set to true.'); + } + + const normalizedPath = normalizePath(pathToMatch); + + return normalizePath(`${normalizedBasePath}${normalizedPath}`) === normalizePath(router.pathname()); + } + + function child(childBasePath: string) { + return createClerkRouter(router, `${normalizedBasePath}${normalizePath(childBasePath)}`); + } + + function push(path: string) { + const destinationUrl = makeDestinationUrlWithPreservedQueryParameters(path); + return router.push(destinationUrl); + } + + function replace(path: string) { + const destinationUrl = makeDestinationUrlWithPreservedQueryParameters(path); + return router.replace(destinationUrl); + } + + function shallowPush(path: string) { + const destinationUrl = makeDestinationUrlWithPreservedQueryParameters(path); + return router.shallowPush(destinationUrl); + } + + return { + child, + match, + mode: router.mode, + name: router.name, + push, + replace, + shallowPush, + pathname: router.pathname, + searchParams: router.searchParams, + basePath: normalizedBasePath, + }; +} diff --git a/packages/shared/src/router/types.ts b/packages/shared/src/router/types.ts new file mode 100644 index 00000000000..d0f948e71d0 --- /dev/null +++ b/packages/shared/src/router/types.ts @@ -0,0 +1 @@ +export type RoutingMode = 'path' | 'virtual'; diff --git a/packages/shared/src/url.ts b/packages/shared/src/url.ts index 5d691f09376..651d0453e75 100644 --- a/packages/shared/src/url.ts +++ b/packages/shared/src/url.ts @@ -170,3 +170,10 @@ export function joinURL(base: string, ...input: string[]): string { return url; } + +/* Code below is taken from https://github.com/vercel/next.js/blob/fe7ff3f468d7651a92865350bfd0f16ceba27db5/packages/next/src/shared/lib/utils.ts. LICENSE: MIT */ + +// Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 +// 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); diff --git a/packages/shared/subpaths.mjs b/packages/shared/subpaths.mjs index 3ec9dc42cdb..0cefd4bb1b8 100644 --- a/packages/shared/subpaths.mjs +++ b/packages/shared/subpaths.mjs @@ -29,6 +29,7 @@ export const subpathNames = [ 'telemetry', 'logger', 'webauthn', + 'router', ]; export const subpathFoldersBarrel = ['react']; From b421630f348617ae60f5f90318ac25f422b255a4 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Wed, 28 Aug 2024 13:37:11 -0500 Subject: [PATCH 2/4] Use router from shared --- .../src/react/router/__tests__/router.test.ts | 129 --------------- packages/elements/src/react/router/index.ts | 4 +- packages/elements/src/react/router/next.ts | 3 +- packages/elements/src/react/router/react.tsx | 47 ------ packages/elements/src/react/router/router.ts | 152 ------------------ packages/elements/src/react/router/virtual.ts | 3 +- 6 files changed, 3 insertions(+), 335 deletions(-) delete mode 100644 packages/elements/src/react/router/__tests__/router.test.ts delete mode 100644 packages/elements/src/react/router/react.tsx delete mode 100644 packages/elements/src/react/router/router.ts diff --git a/packages/elements/src/react/router/__tests__/router.test.ts b/packages/elements/src/react/router/__tests__/router.test.ts deleted file mode 100644 index df1e5a3f497..00000000000 --- a/packages/elements/src/react/router/__tests__/router.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { createClerkRouter } from '../router'; - -describe('createClerkRouter', () => { - const mockRouter = { - name: 'mockRouter', - mode: 'path' as const, - pathname: jest.fn(), - searchParams: jest.fn(), - push: jest.fn(), - shallowPush: jest.fn(), - replace: jest.fn(), - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('creates a ClerkRouter instance with the correct base path', () => { - const oneBasePath = '/app'; - const twoBasePath = 'app'; - const threeBasePath = 'app/'; - const one = createClerkRouter(mockRouter, oneBasePath); - const two = createClerkRouter(mockRouter, twoBasePath); - const three = createClerkRouter(mockRouter, threeBasePath); - - expect(one.basePath).toBe(oneBasePath); - expect(two.basePath).toBe('/app'); - expect(three.basePath).toBe('/app'); - }); - - it('matches the path correctly', () => { - const path = '/dashboard'; - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.pathname.mockReturnValue('/app/dashboard'); - - expect(clerkRouter.match(path)).toBe(true); - }); - - it('normalizes path arguments internally', () => { - const path = 'dashboard/'; - const clerkRouter = createClerkRouter(mockRouter, 'app/'); - - mockRouter.pathname.mockReturnValue('/app/dashboard'); - - expect(clerkRouter.match(path)).toBe(true); - }); - - it('throws an error when no path is provided', () => { - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - expect(() => { - clerkRouter.match(); - }).toThrow('[clerk] router.match() requires either a path to match, or the index flag must be set to true.'); - }); - - it('creates a child router with the correct base path', () => { - const clerkRouter = createClerkRouter(mockRouter, '/app'); - const childRouter = clerkRouter.child('dashboard'); - - expect(childRouter.basePath).toBe('/app/dashboard'); - }); - - it('pushes the correct destination URL ', () => { - const path = '/app/dashboard'; - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.searchParams.mockImplementation(() => new URLSearchParams('')); - clerkRouter.push(path); - - expect(mockRouter.push).toHaveBeenCalledWith('/app/dashboard'); - }); - - it('replaces the correct destination URL', () => { - const path = '/app/dashboard'; - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.searchParams.mockImplementation(() => new URLSearchParams('')); - clerkRouter.replace(path); - - expect(mockRouter.replace).toHaveBeenCalledWith('/app/dashboard'); - }); - - it('pushes the correct destination URL with preserved query parameters', () => { - const path = '/app/dashboard'; - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar')); - clerkRouter.push(path); - - expect(mockRouter.push).toHaveBeenCalledWith('/app/dashboard?after_sign_in_url=foobar'); - }); - - it('replaces the correct destination URL with preserved query parameters', () => { - const path = '/app/dashboard'; - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar')); - clerkRouter.replace(path); - - expect(mockRouter.replace).toHaveBeenCalledWith('/app/dashboard?after_sign_in_url=foobar'); - }); - - it('pushes absolute URLs unmodified', () => { - const path = 'https://example.com'; - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.searchParams.mockImplementation(() => new URLSearchParams('after_sign_in_url=foobar&foo=bar')); - clerkRouter.push(path); - - expect(mockRouter.push).toHaveBeenCalledWith('https://example.com'); - }); - - it('returns the correct pathname', () => { - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.pathname.mockReturnValue('/app/dashboard'); - - expect(clerkRouter.pathname()).toBe('/app/dashboard'); - }); - - it('returns the correct searchParams', () => { - const clerkRouter = createClerkRouter(mockRouter, '/app'); - - mockRouter.searchParams.mockImplementation(() => new URLSearchParams('foo=bar')); - - expect(clerkRouter.searchParams().get('foo')).toEqual('bar'); - }); -}); diff --git a/packages/elements/src/react/router/index.ts b/packages/elements/src/react/router/index.ts index 4b634b9c9e9..5b0979e8a25 100644 --- a/packages/elements/src/react/router/index.ts +++ b/packages/elements/src/react/router/index.ts @@ -1,5 +1,3 @@ export { useNextRouter } from './next'; -export { Route, Router, useClerkRouter } from './react'; +export { Route, Router, useClerkRouter } from '@clerk/shared/router'; export { useVirtualRouter } from './virtual'; - -export type { ClerkRouter, ClerkHostRouter } from './router'; diff --git a/packages/elements/src/react/router/next.ts b/packages/elements/src/react/router/next.ts index 71f7a6a8b12..d8a8ac96a17 100644 --- a/packages/elements/src/react/router/next.ts +++ b/packages/elements/src/react/router/next.ts @@ -1,9 +1,8 @@ +import type { ClerkHostRouter } from '@clerk/shared/router'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { NEXT_WINDOW_HISTORY_SUPPORT_VERSION } from '~/internals/constants'; -import type { ClerkHostRouter } from './router'; - /** * Clerk router integration with Next.js's router. */ diff --git a/packages/elements/src/react/router/react.tsx b/packages/elements/src/react/router/react.tsx deleted file mode 100644 index 79af937b554..00000000000 --- a/packages/elements/src/react/router/react.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { createContext, useContext } from 'react'; - -import type { ClerkHostRouter, ClerkRouter } from './router'; -import { createClerkRouter } from './router'; - -export const ClerkRouterContext = createContext(null); - -export function useClerkRouter() { - const ctx = useContext(ClerkRouterContext); - - if (!ctx) { - throw new Error('clerk: Unable to locate ClerkRouter, make sure this is rendered within ``.'); - } - - return ctx; -} - -export function Router({ - basePath, - children, - router, -}: { - children: React.ReactNode; - basePath?: string; - router: ClerkHostRouter; -}) { - const clerkRouter = createClerkRouter(router, basePath); - - return {children}; -} - -type RouteProps = { path?: string; index?: boolean }; - -export function Route({ path, children, index }: RouteProps & { children: React.ReactNode }) { - // check for parent router, if exists, create child router, otherwise create one - const parentRouter = useClerkRouter(); - - if (!path && !index) { - return children; - } - - if (!parentRouter?.match(path, index)) { - return null; - } - - return children; -} diff --git a/packages/elements/src/react/router/router.ts b/packages/elements/src/react/router/router.ts deleted file mode 100644 index 125755abb9b..00000000000 --- a/packages/elements/src/react/router/router.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { withLeadingSlash, withoutTrailingSlash } from '@clerk/shared/url'; - -import type { ROUTING } from '~/internals/constants'; -import { isAbsoluteUrl } from '~/utils/is-absolute-url'; - -export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up_url', 'redirect_url']; - -/** - * This type represents a generic router interface that Clerk relies on to interact with the host router. - */ -export type ClerkHostRouter = { - readonly mode: ROUTING; - readonly name: string; - pathname: () => string; - push: (path: string) => void; - replace: (path: string) => void; - searchParams: () => URLSearchParams; - shallowPush: (path: string) => void; -}; - -/** - * Internal Clerk router, used by Clerk components to interact with the host's router. - */ -export type ClerkRouter = { - /** - * The basePath the router is currently mounted on. - */ - basePath: string; - /** - * Creates a child router instance scoped to the provided base path. - */ - child: (childBasePath: string) => ClerkRouter; - /** - * Matches the provided path against the router's current path. If index is provided, matches against the root route of the router. - */ - match: (path?: string, index?: boolean) => boolean; - - /** - * Mode of the router instance, path-based or virtual - */ - readonly mode: ROUTING; - - /** - * Name of the router instance - */ - readonly name: string; - - /** - * Navigates to the provided path via a history push - */ - push: ClerkHostRouter['push']; - /** - * Navigates to the provided path via a history replace - */ - replace: ClerkHostRouter['replace']; - /** - * If supported by the host router, navigates to the provided path without triggering a full navigation - */ - shallowPush: ClerkHostRouter['shallowPush']; - /** - * Returns the current pathname (including the base path) - */ - pathname: ClerkHostRouter['pathname']; - /** - * Returns the current search params - */ - searchParams: ClerkHostRouter['searchParams']; -}; - -/** - * Ensures the provided path has a leading slash and no trailing slash - */ -function normalizePath(path: string) { - return withoutTrailingSlash(withLeadingSlash(path)); -} - -/** - * Factory function to create an instance of ClerkRouter with the provided host router. - * - * @param router host router instance to be used by the router - * @param basePath base path of the router, navigation and matching will be scoped to this path - * @returns A ClerkRouter instance - */ -export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/'): ClerkRouter { - const normalizedBasePath = normalizePath(basePath); - - /** - * Certain query parameters need to be preserved when navigating internally. These query parameters are ultimately used by Clerk to dictate behavior, so we keep them around. - */ - function makeDestinationUrlWithPreservedQueryParameters(path: string) { - // If the provided path is an absolute URL, return it unmodified. - if (isAbsoluteUrl(path)) { - return path; - } - - const destinationUrl = new URL(path, window.location.origin); - const currentSearchParams = router.searchParams(); - - PRESERVED_QUERYSTRING_PARAMS.forEach(key => { - const maybeValue = currentSearchParams.get(key); - if (maybeValue) { - destinationUrl.searchParams.set(key, maybeValue); - } - }); - - return `${destinationUrl.pathname}${destinationUrl.search}`; - } - - function match(path?: string, index?: boolean) { - const pathToMatch = path ?? (index && '/'); - - if (!pathToMatch) { - throw new Error('[clerk] router.match() requires either a path to match, or the index flag must be set to true.'); - } - - const normalizedPath = normalizePath(pathToMatch); - - return normalizePath(`${normalizedBasePath}${normalizedPath}`) === normalizePath(router.pathname()); - } - - function child(childBasePath: string) { - return createClerkRouter(router, `${normalizedBasePath}${normalizePath(childBasePath)}`); - } - - function push(path: string) { - const destinationUrl = makeDestinationUrlWithPreservedQueryParameters(path); - return router.push(destinationUrl); - } - - function replace(path: string) { - const destinationUrl = makeDestinationUrlWithPreservedQueryParameters(path); - return router.replace(destinationUrl); - } - - function shallowPush(path: string) { - const destinationUrl = makeDestinationUrlWithPreservedQueryParameters(path); - return router.shallowPush(destinationUrl); - } - - return { - child, - match, - mode: router.mode, - name: router.name, - push, - replace, - shallowPush, - pathname: router.pathname, - searchParams: router.searchParams, - basePath: normalizedBasePath, - }; -} diff --git a/packages/elements/src/react/router/virtual.ts b/packages/elements/src/react/router/virtual.ts index 88218fa3f28..40977e77a24 100644 --- a/packages/elements/src/react/router/virtual.ts +++ b/packages/elements/src/react/router/virtual.ts @@ -1,9 +1,8 @@ 'use client'; +import type { ClerkHostRouter } from '@clerk/shared/router'; import { useSyncExternalStore } from 'react'; -import type { ClerkHostRouter } from './router'; - const DUMMY_ORIGIN = 'https://clerk.dummy'; // TODO: introduce history stack? From 2bcb3f67487c322f3bb265cc12d9778303886a60 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Wed, 28 Aug 2024 13:39:22 -0500 Subject: [PATCH 3/4] Adds changeset --- .changeset/quiet-lobsters-notice.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/quiet-lobsters-notice.md diff --git a/.changeset/quiet-lobsters-notice.md b/.changeset/quiet-lobsters-notice.md new file mode 100644 index 00000000000..88271cbd804 --- /dev/null +++ b/.changeset/quiet-lobsters-notice.md @@ -0,0 +1,6 @@ +--- +"@clerk/shared": minor +"@clerk/elements": patch +--- + +Moves the common `ClerkRouter` interface into `@clerk/shared/router`. Elements has been refactored internally to import the router from the shared package. From c5184460bd98486872f55004854eb38c7a5884f1 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Wed, 28 Aug 2024 13:47:42 -0500 Subject: [PATCH 4/4] fix import --- packages/elements/src/internals/machines/types/router.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/elements/src/internals/machines/types/router.types.ts b/packages/elements/src/internals/machines/types/router.types.ts index a77bac78286..fa32861bf87 100644 --- a/packages/elements/src/internals/machines/types/router.types.ts +++ b/packages/elements/src/internals/machines/types/router.types.ts @@ -1,3 +1,4 @@ +import type { ClerkRouter } from '@clerk/shared/router'; import type { ClerkResource, LoadedClerk, @@ -10,7 +11,6 @@ import type { ActorRefFrom } from 'xstate'; import type { ClerkElementsError } from '~/internals/errors'; import type { TFormMachine } from '~/internals/machines/form'; -import type { ClerkRouter } from '~/react/router'; // ---------------------------------- Events ---------------------------------- //