Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/brown-suns-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@clerk/astro": patch
"@clerk/nextjs": patch
"@clerk/shared": patch
---

Previously the `createPathMatcher()` function was re-implemented both in `@clerk/astro` and `@clerk/nextjs`, this PR moves this logic to `@clerk/shared`.

You can use it like so:

```ts
import { createPathMatcher } from '@clerk/shared/pathMatcher'
```
19 changes: 4 additions & 15 deletions packages/astro/src/server/route-matcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { pathToRegexp } from '@clerk/shared/pathToRegexp';
import type { Autocomplete } from '@clerk/types';
import { createPathMatcher, type PathMatcherParam } from '@clerk/shared/pathMatcher';

type WithPathPatternWildcard<T = string> = `${T & string}(.*)`;
export type RouteMatcherParam = PathMatcherParam;

type RouteMatcherRoutes = Autocomplete<WithPathPatternWildcard>;

export type RouteMatcherParam = Array<RegExp | RouteMatcherRoutes> | RegExp | RouteMatcherRoutes;

// TODO-SHARED: This can be moved to @clerk/shared as an identical implementation exists in @clerk/nextjs
/**
* Returns a function that accepts a `Request` object and returns whether the request matches the list of
* predefined routes that can be passed in as the first argument.
Expand All @@ -17,11 +11,6 @@ export type RouteMatcherParam = Array<RegExp | RouteMatcherRoutes> | RegExp | Ro
* For more information, see: https://clerk.com/docs
*/
export const createRouteMatcher = (routes: RouteMatcherParam) => {
const routePatterns = [routes || ''].flat().filter(Boolean);
const matchers = precomputePathRegex(routePatterns);
return (req: Request) => matchers.some(matcher => matcher.test(new URL(req.url).pathname));
};

const precomputePathRegex = (patterns: Array<string | RegExp>) => {
return patterns.map(pattern => (pattern instanceof RegExp ? pattern : pathToRegexp(pattern)));
const matcher = createPathMatcher(routes);
return (req: Request) => matcher(new URL(req.url).pathname);
};
13 changes: 3 additions & 10 deletions packages/nextjs/src/server/routeMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { pathToRegexp } from '@clerk/shared/pathToRegexp';
import { createPathMatcher, type WithPathPatternWildcard } from '@clerk/shared/pathMatcher';
import type { Autocomplete } from '@clerk/types';
import type Link from 'next/link';
import type { NextRequest } from 'next/server';

type WithPathPatternWildcard<T> = `${T & string}(.*)`;
type NextTypedRoute<T = Parameters<typeof Link>['0']['href']> = T extends string ? T : never;

type RouteMatcherWithNextTypedRoutes = Autocomplete<WithPathPatternWildcard<NextTypedRoute> | NextTypedRoute>;

export type RouteMatcherParam =
Expand All @@ -27,11 +25,6 @@ export const createRouteMatcher = (routes: RouteMatcherParam) => {
return (req: NextRequest) => routes(req);
}

const routePatterns = [routes || ''].flat().filter(Boolean);
const matchers = precomputePathRegex(routePatterns);
return (req: NextRequest) => matchers.some(matcher => matcher.test(req.nextUrl.pathname));
};

const precomputePathRegex = (patterns: Array<string | RegExp>) => {
return patterns.map(pattern => (pattern instanceof RegExp ? pattern : pathToRegexp(pattern)));
const matcher = createPathMatcher(routes);
return (req: NextRequest) => matcher(req.nextUrl.pathname);
};
3 changes: 2 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
"devBrowser",
"object",
"oauth",
"web3"
"web3",
"pathMatcher"
],
"scripts": {
"build": "tsup",
Expand Down
53 changes: 53 additions & 0 deletions packages/shared/src/__tests__/pathMatcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createPathMatcher } from '../pathMatcher';

jest.mock('../pathToRegexp', () => ({
pathToRegexp: (pattern: string) => new RegExp(`^${pattern.replace('(.*)', '.*')}$`),
}));

describe('createPathMatcher', () => {
test('matches exact paths', () => {
const matcher = createPathMatcher('/foo');
expect(matcher('/foo')).toBe(true);
expect(matcher('/bar')).toBe(false);
});

test('matches wildcard patterns', () => {
const matcher = createPathMatcher('/foo(.*)');
expect(matcher('/foo')).toBe(true);
expect(matcher('/foo/bar')).toBe(true);
expect(matcher('/foo/bar/baz')).toBe(true);
expect(matcher('/bar')).toBe(false);
});

test('matches array of patterns', () => {
const matcher = createPathMatcher(['/foo', '/bar(.*)']);
expect(matcher('/foo')).toBe(true);
expect(matcher('/bar')).toBe(true);
expect(matcher('/bar/baz')).toBe(true);
expect(matcher('/baz')).toBe(false);
expect(matcher('/foo/bar')).toBe(false);
});

test('matches RegExp patterns', () => {
const matcher = createPathMatcher(/^\/foo\/.*$/);
expect(matcher('/foo/bar')).toBe(true);
expect(matcher('/foo/baz')).toBe(true);
expect(matcher('/bar/foo')).toBe(false);
});

test('handles empty or falsy inputs', () => {
const matcher = createPathMatcher('');
expect(matcher('/any/path')).toBe(false);

const nullMatcher = createPathMatcher(null as any);
expect(nullMatcher('/any/path')).toBe(false);
});

test('handles mixed pattern types', () => {
const matcher = createPathMatcher(['/foo(.*)', /^\/bar\/.*$/, '/baz']);
expect(matcher('/foo/anything')).toBe(true);
expect(matcher('/bar/anything')).toBe(true);
expect(matcher('/baz')).toBe(true);
expect(matcher('/qux')).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from './object';
export * from './logger';
export { createWorkerTimers } from './workerTimers';
export { DEV_BROWSER_JWT_KEY, extractDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser';
export * from './pathMatcher';
23 changes: 23 additions & 0 deletions packages/shared/src/pathMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Autocomplete } from '@clerk/types';

import { pathToRegexp } from './pathToRegexp';

export type WithPathPatternWildcard<T = string> = `${T & string}(.*)`;
export type PathPattern = Autocomplete<WithPathPatternWildcard>;
export type PathMatcherParam = Array<RegExp | PathPattern> | RegExp | PathPattern;

const precomputePathRegex = (patterns: Array<string | RegExp>) => {
return patterns.map(pattern => (pattern instanceof RegExp ? pattern : pathToRegexp(pattern)));
};

/**
* Creates a function that matches paths against a set of patterns.
*
* @param patterns - A string, RegExp, or array of patterns to match against
* @returns A function that takes a pathname and returns true if it matches any of the patterns
*/
export const createPathMatcher = (patterns: PathMatcherParam) => {
const routePatterns = [patterns || ''].flat().filter(Boolean);
const matchers = precomputePathRegex(routePatterns);
return (pathname: string) => matchers.some(matcher => matcher.test(pathname));
};