diff --git a/.changeset/mighty-boats-wink.md b/.changeset/mighty-boats-wink.md
new file mode 100644
index 00000000..2ebccec5
--- /dev/null
+++ b/.changeset/mighty-boats-wink.md
@@ -0,0 +1,5 @@
+---
+"react-resource-router": minor
+---
+
+Updated the path regex matching method added test to make sure complex routing is not broken
diff --git a/docs/router/configuration.md b/docs/router/configuration.md
index 1e8fec7e..fe262c47 100644
--- a/docs/router/configuration.md
+++ b/docs/router/configuration.md
@@ -48,6 +48,8 @@ export const routes = [
];
```
+You can also mark a path param as optional by using the `?` suffix. For example, if you want to make the `userId` param optional, you would do so like this `'/user/:userId?'`.
+
## History
You must provide a `history` instance to the router. Again, this will feel familiar to users of `react-router`. Here is how to do this
diff --git a/package-lock.json b/package-lock.json
index a223f9ed..ba014aee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"lodash.debounce": "^4.0.8",
"lodash.noop": "^3.0.1",
- "path-to-regexp": "^1.7.0",
+ "path-to-regexp": "^6.2.1",
"react-sweet-state": "^2.6.4",
"url-parse": "^1.5.10"
},
@@ -15252,12 +15252,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "license": "MIT"
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -20067,13 +20061,9 @@
}
},
"node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "license": "MIT",
- "dependencies": {
- "isarray": "0.0.1"
- }
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -36757,11 +36747,6 @@
"integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==",
"dev": true
},
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
- },
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -40282,12 +40267,9 @@
}
},
"path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "requires": {
- "isarray": "0.0.1"
- }
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"path-type": {
"version": "4.0.0",
diff --git a/package.json b/package.json
index bfe041da..2cfa45d9 100644
--- a/package.json
+++ b/package.json
@@ -66,7 +66,7 @@
"dependencies": {
"lodash.debounce": "^4.0.8",
"lodash.noop": "^3.0.1",
- "path-to-regexp": "^1.7.0",
+ "path-to-regexp": "^6.2.1",
"react-sweet-state": "^2.6.4",
"url-parse": "^1.5.10"
},
diff --git a/src/__tests__/integration.test.tsx b/src/__tests__/integration.test.tsx
index 97292d30..b32586f7 100644
--- a/src/__tests__/integration.test.tsx
+++ b/src/__tests__/integration.test.tsx
@@ -5,7 +5,14 @@ import React, { StrictMode } from 'react';
import { defaultRegistry } from 'react-sweet-state';
import { isServerEnvironment } from '../common/utils/is-server-environment';
-import { Route, RouteComponent, Router, type Plugin } from '../index';
+import {
+ Route,
+ RouteComponent,
+ Router,
+ type Plugin,
+ usePathParam,
+ useQueryParam,
+} from '../index';
jest.mock('../common/utils/is-server-environment');
@@ -318,5 +325,209 @@ describe(' client-side integration tests', () => {
expect(screen.getByText('route component')).toBeInTheDocument();
});
});
+
+ describe(`path matching integration tests: strict mode ${strictModeState}`, () => {
+ it('matches dynamic route with optional parameter', () => {
+ const MigrationComponent = () => {
+ const [step] = usePathParam('step');
+ const [migrationId] = usePathParam('migrationId');
+
+ return (
+
+ Step: {step}, Migration ID: {migrationId || 'N/A'}
+
+ );
+ };
+
+ const route = {
+ name: 'migration',
+ path: '/settings/system/migration/:step/:migrationId?',
+ component: MigrationComponent,
+ };
+ const { history } = mountRouter({ routes: [route], strictMode: true });
+
+ act(() => {
+ history.push('/settings/system/migration/plan-configuration/123');
+ });
+
+ expect(
+ screen.getByText('Step: plan-configuration, Migration ID: 123')
+ ).toBeInTheDocument();
+ });
+
+ it('matches route with regex constraint on path parameter', () => {
+ const PlanComponent = () => {
+ const [planId] = usePathParam('planId');
+
+ return Plan ID: {planId}
;
+ };
+
+ const route = {
+ name: 'plans',
+ path: '/plans/:planId(\\d+)',
+ component: PlanComponent,
+ };
+ const { history } = mountRouter({ routes: [route], strictMode: true });
+
+ act(() => {
+ history.push('/plans/456');
+ });
+
+ expect(screen.getByText('Plan ID: 456')).toBeInTheDocument();
+ });
+
+ it('matches route with multiple dynamic parameters', () => {
+ const ProjectAppComponent = () => {
+ const [projectType] = usePathParam('projectType');
+ const [projectKey] = usePathParam('projectKey');
+ const [appId] = usePathParam('appId');
+
+ return (
+
+ Project Type: {projectType}, Project Key: {projectKey}, App ID:{' '}
+ {appId}
+
+ );
+ };
+
+ const route = {
+ name: 'project-app',
+ path: '/app/:projectType(software|servicedesk)/projects/:projectKey/apps/:appId',
+ component: ProjectAppComponent,
+ };
+ const { history } = mountRouter({ routes: [route], strictMode: true });
+
+ act(() => {
+ history.push('/app/software/projects/PROJ123/apps/456');
+ });
+
+ expect(
+ screen.getByText(
+ 'Project Type: software, Project Key: PROJ123, App ID: 456'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('matches route with dynamic and query parameters', () => {
+ const IssueComponent = () => {
+ const [issueKey] = usePathParam('issueKey');
+ const [queryParam] = useQueryParam('query');
+
+ return (
+
+ Issue Key: {issueKey}, Query: {queryParam || 'None'}
+
+ );
+ };
+
+ const route = {
+ name: 'browse',
+ path: '/browse/:issueKey(\\w+-\\d+)',
+ component: IssueComponent,
+ };
+ const { history } = mountRouter({ routes: [route], strictMode: true });
+
+ act(() => {
+ history.push('/browse/ISSUE-123?query=details');
+ });
+
+ expect(
+ screen.getByText('Issue Key: ISSUE-123, Query: details')
+ ).toBeInTheDocument();
+ });
+
+ it('matches route with complex regex constraint on path parameter and wildcard', () => {
+ const IssueComponent = () => {
+ const [issueKey] = usePathParam('issueKey');
+
+ return Issue Key: {issueKey}
;
+ };
+
+ const route = {
+ name: 'browse',
+ path: '/browse/:issueKey(\\w+-\\d+)(.*)?',
+ component: IssueComponent,
+ };
+ const { history } = mountRouter({ routes: [route], strictMode: true });
+
+ act(() => {
+ history.push('/browse/ISSUE-123/details');
+ });
+
+ expect(screen.getByText('Issue Key: ISSUE-123')).toBeInTheDocument();
+ });
+
+ it('matches route with multiple dynamic segments and regex constraints', () => {
+ const SettingsComponent = () => {
+ const [settingsType] = usePathParam('settingsType');
+ const [appId] = usePathParam('appId');
+ const [envId] = usePathParam('envId');
+ const [route] = usePathParam('route');
+
+ return (
+
+ Settings Type: {settingsType}, App ID: {appId}, Environment ID:{' '}
+ {envId}, Route: {route || 'None'}
+
+ );
+ };
+
+ const route = {
+ name: 'settings',
+ path: '/settings/apps/:settingsType(configure|get-started)/:appId/:envId/:route?',
+ component: SettingsComponent,
+ };
+ const { history } = mountRouter({ routes: [route], strictMode: true });
+
+ act(() => {
+ history.push('/settings/apps/configure/app123/env456/setup');
+ });
+
+ expect(
+ screen.getByText(
+ 'Settings Type: configure, App ID: app123, Environment ID: env456, Route: setup'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('matches route with regex constraint and renders wildcard route for invalid paths', () => {
+ const IssueComponent = () => {
+ const [issueKey] = usePathParam('issueKey');
+
+ return Issue Key: {issueKey}
;
+ };
+
+ const NotFoundComponent = () => Not Found
;
+
+ const routes = [
+ {
+ name: 'issue',
+ path: '/browse/:issueKey(\\w+-\\d+)(.*)?',
+ component: IssueComponent,
+ },
+ {
+ name: 'wildcard',
+ path: '/',
+ component: NotFoundComponent,
+ },
+ ];
+ const { history } = mountRouter({ routes, strictMode: true });
+
+ act(() => {
+ history.push('/browse/TEST-1');
+ });
+ expect(screen.getByText('Issue Key: TEST-1')).toBeInTheDocument();
+
+ act(() => {
+ history.push('/browse/1');
+ });
+ expect(screen.getByText('Not Found')).toBeInTheDocument();
+
+ act(() => {
+ history.push('/browse/TEST');
+ });
+ expect(screen.getByText('Not Found')).toBeInTheDocument();
+ });
+ });
}
});
diff --git a/src/common/utils/match-route/matchPath.ts b/src/common/utils/match-route/matchPath.ts
index 8d73d743..0f207978 100644
--- a/src/common/utils/match-route/matchPath.ts
+++ b/src/common/utils/match-route/matchPath.ts
@@ -1,6 +1,6 @@
// TAKEN FROM https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/matchPath.js
-import pathToRegexp from 'path-to-regexp';
+import { pathToRegexp } from 'path-to-regexp';
const cache: { [key: string]: any } = {};
const cacheLimit = 10000;