feat(core): routing - path-to-regexp
MODIFY
packages/core/package.json
packages/core/package.json
},
"dependencies": {
"file-type": "^8.0.0",
- "mime": "^2.3.1"
+ "mime": "^2.3.1",
+ "path-to-regexp": "^2.4.0"
},
"devDependencies": {
+ "@types/path-to-regexp": "^1.7.0",
"@types/file-type": "^5.2.1",
"@types/mime": "^2.0.0",
"webpack": "^4.17.2"

MODIFY
packages/core/src/router/router.factory.ts
packages/core/src/router/router.factory.ts
@@ -30,7 +30,7 @@ export const factorizeRouting = (
));
}
- const { regExp, parameters } = factorizeRegExpWithParams(concatenatedPath);
+ const { regExp, parameters, path } = factorizeRegExpWithParams(concatenatedPath);
const foundRoute = routing.find(route => route.regExp.source === regExp.source);
const method: RoutingMethod = {
effect: route.effect,
@@ -49,6 +49,7 @@ export const factorizeRouting = (
}
return routing.push({
+ path,
regExp,
methods: { [route.method]: method },
} as RoutingItem);

MODIFY
packages/core/src/router/router.interface.ts
packages/core/src/router/router.interface.ts
@@ -24,6 +24,7 @@ export interface RouteCombinerConfig {
export interface ParametricRegExp {
regExp: RegExp;
parameters?: string[] | undefined;
+ path: string;
}
export interface RoutingMethod {
@@ -34,6 +35,7 @@ export interface RoutingMethod {
export interface RoutingItem {
regExp: RegExp;
+ path: string;
methods: Partial<Record<HttpMethod, RoutingMethod>>;
}

MODIFY
packages/core/src/router/specs/router.factory.spec.ts
packages/core/src/router/specs/router.factory.spec.ts
@@ -50,22 +50,26 @@ describe('Router factory', () => {
// then
expect(factorizedRouting).toEqual([
{
- regExp: /^\/?$/,
+ regExp: /^(?:\/)?$/i,
+ path: '',
methods: { GET: { effect: e1$, middleware: undefined, parameters: undefined } },
},
{
- regExp: /^\/([^\/]+)\/?$/,
+ regExp: /^\/([^\/]+?)(?:\/)?$/i,
+ path: '/:id',
methods: {
GET: { middleware: m$, effect: e2$, parameters: ['id'] },
POST: { middleware: m$, effect: e3$, parameters: ['id'] },
},
},
{
- regExp: /^\/([^\/]+)\/nested\/?$/,
+ regExp: /^\/([^\/]+?)\/nested(?:\/)?$/i,
+ path: '/:id/nested',
methods: { GET: { middleware: m$, effect: e5$, parameters: ['id'] } },
},
{
- regExp: /^\/([^\/]+)\/.*?$/,
+ regExp: /^\/([^\/]+?)\/(.*)(?:\/)?$/i,
+ path: '/:id/(.*)',
methods: { '*': { middleware: m$, effect: e4$, parameters: ['id'] } },
},
] as Routing);

MODIFY
packages/core/src/router/specs/router.spec.ts
packages/core/src/router/specs/router.spec.ts
@@ -15,9 +15,21 @@ describe('Router', () => {
const e4$: Effect = req$ => req$.pipe(mapTo({ body: 'test' }));
const routing: Routing = [
- { regExp: /^\/?$/, methods: { GET: { effect: e1$ }, POST: { effect: e4$ } } },
- { regExp: /^\/group\/?$/, methods: { GET: { effect: e2$ } } },
- { regExp: /^\/group\/nested\/foo\/?$/, methods: { POST: { effect: e3$ } } },
+ {
+ regExp: /^\/?$/,
+ path: '',
+ methods: { GET: { effect: e1$ }, POST: { effect: e4$ } },
+ },
+ {
+ regExp: /^\/group\/?$/,
+ path: '/group',
+ methods: { GET: { effect: e2$ } },
+ },
+ {
+ regExp: /^\/group\/nested\/foo\/?$/,
+ path: '/group/nested/foo',
+ methods: { POST: { effect: e3$ } },
+ },
];
// when
@@ -39,6 +51,7 @@ describe('Router', () => {
const routing: Routing = [{
regExp: /^\/group\/([^\/]+)\/foo$/,
+ path: '/group/:param/foo',
methods: { GET: { effect: e$, parameters: ['param'] } }
}];
@@ -58,8 +71,16 @@ describe('Router', () => {
const e2$: Effect = req$ => req$.pipe(mapTo({ body: 'e2' }));
const routing: Routing = [
- { regExp: /^\/group\/nested\/?$/, methods: { GET: { effect: e1$ } } },
- { regExp: /^\/group.*?$/, methods: { '*': { effect: e2$ } } }
+ {
+ regExp: /^\/group\/nested\/?$/,
+ path: '/group/nested',
+ methods: { GET: { effect: e1$ } },
+ },
+ {
+ regExp: /^\/group.*?$/,
+ path: '/group',
+ methods: { '*': { effect: e2$ } },
+ }
];
// when

MODIFY
packages/core/src/router/specs/urlParams.factory.spec.ts
packages/core/src/router/specs/urlParams.factory.spec.ts
@@ -20,7 +20,7 @@ describe('Url parameters factory', () => {
// when
const { regExp, parameters } = factorizeRegExpWithParams(path);
- const match = '/foo/test1/bar/test2/baz'.match(regExp);
+ const match = regExp.exec('/foo/test1/bar/test2/baz');
// then
expect(parameters).toEqual([ 'param1', 'param2' ]);
@@ -38,7 +38,7 @@ describe('Url parameters factory', () => {
const correctRegexp = factorizeRegExpWithParams(correctPath);
// then
- expect(wrongRegexp.parameters).toEqual(['param1:param2']);
+ expect(wrongRegexp.parameters).toEqual(['param1', 'param2']);
expect(correctRegexp.parameters).toEqual(['param1', 'param2']);
});
@@ -62,7 +62,6 @@ describe('Url parameters factory', () => {
const { regExp } = factorizeRegExpWithParams(path);
// then
- expect(regExp).toEqual(/^\/foo\/bar\/baz$/);
expect(regExp.test('/foo/bar/baz')).toEqual(true);
});
@@ -74,7 +73,6 @@ describe('Url parameters factory', () => {
const { regExp } = factorizeRegExpWithParams(path);
// then
- expect(regExp).toEqual(/^\/foo\/([^\/]+)\/bar$/);
expect(regExp.test('/foo/test/bar')).toEqual(true);
});
@@ -86,22 +84,21 @@ describe('Url parameters factory', () => {
const { regExp } = factorizeRegExpWithParams(path);
// then
- expect(regExp).toEqual(/^\/foo\/.*?\/bar$/);
expect(regExp.test('/foo/test/bar')).toEqual(true);
});
test('matches RegExpt wildcard at the end of the path', () => {
// given
- const path = '/foo/*';
+ const pathToFactorize = '/foo/*';
// when
- const { regExp } = factorizeRegExpWithParams(path);
+ const { regExp } = factorizeRegExpWithParams(pathToFactorize);
// then
- expect(regExp).toEqual(/^\/foo\/.*?$/);
expect(regExp.test('/foo/test1/test2')).toEqual(true);
expect(regExp.test('/foo/test1')).toEqual(true);
expect(regExp.test('/foo/')).toEqual(true);
+ expect(regExp.test('/foo2')).toEqual(false);
});
test('matches parameter at the end of the path', () => {
@@ -114,8 +111,6 @@ describe('Url parameters factory', () => {
const match = testUrl.match(regExp);
// then
- expect(regExp).toEqual(/^\/foo\/([^\/]+)$/);
-
if (match) {
expect(match[1]).toEqual('test');
} else {
@@ -123,36 +118,34 @@ describe('Url parameters factory', () => {
}
});
- xtest('matches wildcard parameter at the end of the path', () => {
+ test('matches wildcard parameter at the end of the path', () => {
// given
- const path = '/foo/:param*';
+ const pathToFactorize = '/foo/:param*';
const testUrl1 = '/foo/test1/test2';
const testUrl2 = '/foo/test1';
const testUrl3 = '/foo/';
// when
- const { regExp } = factorizeRegExpWithParams(path);
+ const { regExp } = factorizeRegExpWithParams(pathToFactorize);
const match1 = testUrl1.match(regExp);
const match2 = testUrl2.match(regExp);
const match3 = testUrl3.match(regExp);
// then
- expect(regExp).toEqual(/^\/foo\/([^\/]+)$/);
-
if (match1) {
- expect(match1[1]).toEqual('/test1/test2');
+ expect(match1[1]).toEqual('test1/test2');
} else {
return fail(`${testUrl1} -> Regexp should be matched`);
}
if (match2) {
- expect(match2[1]).toEqual('/test1');
+ expect(match2[1]).toEqual('test1');
} else {
return fail(`${testUrl2} -> Regexp should be matched`);
}
if (match3) {
- expect(match3[1]).toEqual('/');
+ expect(match3[1]).toEqual(undefined);
} else {
return fail(`${testUrl3} -> Regexp should be matched`);
}

MODIFY
packages/core/src/router/urlParams.factory.ts
packages/core/src/router/urlParams.factory.ts
import { ParametricRegExp } from './router.interface';
+import * as pathToRegexp from 'path-to-regexp';
export const factorizeRegExpWithParams = (path: string): ParametricRegExp => {
- // Matches all path params, if they're followed by a series of star characters, stars are omitted in parameter name
- const pathParameters = /:([^\/*]+)[*]*/g;
- // Matches only star path params (including stars in group)
- const starParameters = /:([^\/*]+[*]*)/g;
- const parameters: string[] = [];
+ const keys: pathToRegexp.Key[] = [];
+ const preparedPath = path
+ .replace(/\/\*/g, '/(.*)') /* Transfer wildcards */
+ .replace(/\/\/+/g, '/') /* Remove repeated backslashes */
+ .replace(/\/$/, ''); /* Remove trailing backslash */
- let pathParameterMatch;
- while ((pathParameterMatch = pathParameters.exec(path)) !== null) {
- parameters.push(pathParameterMatch[1]);
- }
-
- const pattern = path
- .replace(/\/\//g, '/') /* Remove duplicate backslashes */
- .replace(/\\\\/g, '\\') /* Remove duplicate slashes */
- .replace(/([?./\\()[\]{}^$])/g, '\\$1') /* Escape all regex characters */
- .replace(pathParameters, `([^\\/]+)`) /* Translate all path parameters to regex groups */
- .replace(starParameters, `(.+)`) /* Translate star path parameters to regex groups not limited by slashes */
- .replace(/\*/g, '.*?') /* Translate all stars to a wildcard */
- .replace(/\/{2,}/g, '/') /* Remove duplicated backslashes */
- .replace(/([/\\])$/, '$1?'); /* Last slash/backslash is always optional */
+ const regExp = pathToRegexp(preparedPath, keys, { strict: false });
+ const regExpParameters = keys
+ .filter(key => key.name !== 0) /* Filter wildcard groups */
+ .map(key => String(key.name));
return {
- regExp: new RegExp('^' + pattern + '$'),
- parameters: parameters.length > 0 ? parameters : undefined,
+ regExp,
+ parameters: regExpParameters.length > 0 ? regExpParameters : undefined,
+ path: preparedPath,
};
};

MODIFY
yarn.lock
yarn.lock
version "10.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897"
+"@types/path-to-regexp@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@types/path-to-regexp/-/path-to-regexp-1.7.0.tgz#f1af9d9c9db5f8ad80aa75ed424db665c48d6cd4"
+ dependencies:
+ path-to-regexp "*"
+
"@types/superagent@*":
version "3.8.3"
resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.3.tgz#51aa6040c21b66df00f46e10bb08fb51c541acfc"
@@ -5106,6 +5112,10 @@ path-parse@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+path-to-regexp@*, path-to-regexp@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704"
+
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"

feat(core): routing - path-to-regexp