From 0e3b1692993fe9ca86bb2b50462e4981d3e09054 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:24:04 -0700 Subject: [PATCH 01/12] Remove script from code coverage --- vitest.config.mts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 vitest.config.mts diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..f98f607 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,10 @@ +/// +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + coverage: { + exclude: ["scripts/**"], + }, + }, +}); From a9909e45da991c558ab4ae443e2a021ac607db8a Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:26:37 -0700 Subject: [PATCH 02/12] Allow parameter names to be quoted --- src/index.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3f0721a..fc5af69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ const DEFAULT_DELIMITER = "/"; const NOOP_VALUE = (value: string) => value; -const ID_CHAR = /^\p{XID_Continue}$/u; +const ID_START = /^[$_\p{ID_Start}]$/u; +const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u; const DEBUG_URL = "https://git.new/pathToRegexpError"; /** @@ -144,12 +145,35 @@ function lexer(str: string) { if (value === ":") { let name = ""; - while (ID_CHAR.test(chars[++i])) { + if (ID_START.test(chars[++i])) { name += chars[i]; + while (ID_CONTINUE.test(chars[++i])) { + name += chars[i]; + } + } else if (chars[i] === '"') { + let pos = i; + + while (i < chars.length) { + if (chars[++i] === '"') { + i++; + pos = 0; + break; + } + + if (chars[i] === "\\") { + name += chars[++i]; + } else { + name += chars[i]; + } + } + + if (pos) { + throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); + } } if (!name) { - throw new TypeError(`Missing parameter name at ${i}`); + throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); } tokens.push({ type: "NAME", index: i, value: name }); From cbf2c73c865e5cf8faeda8564b7917b75c277ae9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:27:16 -0700 Subject: [PATCH 03/12] Add debug URL to all errors --- src/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index fc5af69..2dd3d6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -186,7 +186,9 @@ function lexer(str: string) { let pattern = ""; if (chars[i] === "?") { - throw new TypeError(`Pattern cannot start with "?" at ${i}`); + throw new TypeError( + `Pattern cannot start with "?" at ${i}: ${DEBUG_URL}`, + ); } while (i < chars.length) { @@ -204,15 +206,22 @@ function lexer(str: string) { } else if (chars[i] === "(") { count++; if (chars[i + 1] !== "?") { - throw new TypeError(`Capturing groups are not allowed at ${i}`); + throw new TypeError( + `Capturing groups are not allowed at ${i}: ${DEBUG_URL}`, + ); } } pattern += chars[i++]; } - if (count) throw new TypeError(`Unbalanced pattern at ${pos}`); - if (!pattern) throw new TypeError(`Missing pattern at ${pos}`); + if (count) { + throw new TypeError(`Unbalanced pattern at ${pos}: ${DEBUG_URL}`); + } + + if (!pattern) { + throw new TypeError(`Missing pattern at ${pos}: ${DEBUG_URL}`); + } tokens.push({ type: "PATTERN", index: i, value: pattern }); continue; From 208cc83c58e90081f9255194d70ba1d4eb4f1c8e Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 18:31:21 -0700 Subject: [PATCH 04/12] Refactor params to match up to next token --- Readme.md | 194 ++++--- scripts/redos.ts | 52 +- src/cases.spec.ts | 1363 ++++++++++----------------------------------- src/index.spec.ts | 89 +-- src/index.ts | 310 ++++------- 5 files changed, 584 insertions(+), 1424 deletions(-) diff --git a/Readme.md b/Readme.md index dc267b7..eaa60b2 100644 --- a/Readme.md +++ b/Readme.md @@ -17,51 +17,42 @@ npm install path-to-regexp --save ## Usage ```js -const { pathToRegexp, match, parse, compile } = require("path-to-regexp"); +const { match, compile, parse } = require("path-to-regexp"); -// pathToRegexp(path, options?) // match(path, options?) -// parse(path, options?) // compile(path, options?) +// parse(path, options?) ``` -### Path to regexp +### Match -The `pathToRegexp` function returns a regular expression with `keys` as a property. It accepts the following arguments: +The `match` function returns a function for transforming paths into parameters: - **path** A string. -- **options** _(optional)_ +- **options** _(optional)_ (See [parse](#parse) for more options) - **sensitive** Regexp will be case sensitive. (default: `false`) - - **trailing** Allows optional trailing delimiter to match. (default: `true`) - - **strict** Verify patterns are valid and safe to use. (default: `false`, recommended: `true`) - - **end** Match to the end of the string. (default: `true`) - - **start** Match from the beginning of the string. (default: `true`) - - **loose** Allow the delimiter to be arbitrarily repeated, e.g. `/` or `///`. (default: `true`) - - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding) + - **end** Validate the match reaches the end of the string. (default: `true`) + - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) ```js -const regexp = pathToRegexp("/foo/:bar"); -// regexp = /^\/+foo(?:\/+([^\/]+?))(?:\/+)?$/i -// keys = [{ name: 'bar', prefix: '', suffix: '', pattern: '', modifier: '' }] +const fn = match("/foo/:bar"); ``` -**Please note:** The `RegExp` returned by `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). +**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). ### Parameters -The path argument is used to define parameters and populate keys. +Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. #### Named parameters -Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters (similar to JavaScript). +Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters, similar to JavaScript. ```js -const regexp = pathToRegexp("/:foo/:bar"); -// keys = [{ name: 'foo', ... }, { name: 'bar', ... }] +const fn = match("/:foo/:bar"); -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0 ] +fn("/test/route"); +//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` ##### Custom matching parameters @@ -69,23 +60,21 @@ regexp.exec("/test/route"); Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path: ```js -const regexpNumbers = pathToRegexp("/icon-:foo(\\d+).png"); -// keys = [{ name: 'foo', ... }] +const exampleNumbers = match("/icon-:foo(\\d+).png"); -regexpNumbers.exec("/icon-123.png"); -//=> ['/icon-123.png', '123'] +exampleNumbers("/icon-123.png"); +//=> { path: '/icon-123.png', params: { foo: '123' } } -regexpNumbers.exec("/icon-abc.png"); -//=> null +exampleNumbers("/icon-abc.png"); +//=> false -const regexpWord = pathToRegexp("/(user|u)"); -// keys = [{ name: 0, ... }] +const exampleWord = pathToRegexp("/(user|u)"); -regexpWord.exec("/u"); -//=> ['/u', 'u'] +exampleWord("/u"); +//=> { path: '/u', params: { '0': 'u' } } -regexpWord.exec("/users"); -//=> null +exampleWord("/users"); +//=> false ``` **Tip:** Backslashes need to be escaped with another backslash in JavaScript strings. @@ -95,25 +84,24 @@ regexpWord.exec("/users"); It is possible to define a parameter without a name. The name will be numerically indexed: ```js -const regexp = pathToRegexp("/:foo/(.*)"); -// keys = [{ name: 'foo', ... }, { name: '0', ... }] +const fn = match("/:foo/(.*)"); -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0 ] +fn("/test/route"); +//=> { path: '/test/route', params: { '0': 'route', foo: 'test' } } ``` -##### Custom prefix and suffix +#### Custom prefix and suffix Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: ```js -const regexp = pathToRegexp("{/:attr1}?{-:attr2}?{-:attr3}?"); +const fn = match("{/:attr1}?{-:attr2}?{-:attr3}?"); -regexp.exec("/test"); -// => ['/test', 'test', undefined, undefined] +fn("/test"); +//=> { path: '/test', params: { attr1: 'test' } } -regexp.exec("/test-test"); -// => ['/test', 'test', 'test', undefined] +fn("/test-test"); +//=> { path: '/test-test', params: { attr1: 'test', attr2: 'test' } } ``` #### Modifiers @@ -125,14 +113,13 @@ Modifiers are used after parameters with custom prefixes and suffixes (`{}`). Parameters can be suffixed with a question mark (`?`) to make the parameter optional. ```js -const regexp = pathToRegexp("/:foo{/:bar}?"); -// keys = [{ name: 'foo', ... }, { name: 'bar', prefix: '/', modifier: '?' }] +const fn = match("/:foo{/:bar}?"); -regexp.exec("/test"); -//=> [ '/test', 'test', undefined, index: 0 ] +fn("/test"); +//=> { path: '/test', params: { foo: 'test' } } -regexp.exec("/test/route"); -//=> [ '/test/route', 'test', 'route', index: 0 ] +fn("/test/route"); +//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` ##### Zero or more @@ -140,14 +127,13 @@ regexp.exec("/test/route"); Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches. ```js -const regexp = pathToRegexp("{/:foo}*"); -// keys = [{ name: 'foo', prefix: '/', modifier: '*' }] +const fn = match("{/:foo}*"); -regexp.exec("/foo"); -//=> [ '/foo', "foo", index: 0 ] +fn("/foo"); +//=> { path: '/foo', params: { foo: [ 'foo' ] } } -regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0 ] +fn("/bar/baz"); +//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } ``` ##### One or more @@ -155,14 +141,13 @@ regexp.exec("/bar/baz"); Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches. ```js -const regexp = pathToRegexp("{/:foo}+"); -// keys = [{ name: 'foo', prefix: '/', modifier: '+' }] +const fn = match("{/:foo}+"); -regexp.exec("/"); -//=> null +fn("/"); +//=> false -regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0 ] +fn("/bar/baz"); +//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } ``` ##### Custom separator @@ -170,54 +155,36 @@ regexp.exec("/bar/baz"); By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this: ```js -const regexp = pathToRegexp("/name{/:parts;-}+"); +const fn = match("/name{/:parts;-}+"); -regexp.exec("/name"); -//=> null +fn("/name"); +//=> false -regexp.exec("/bar/1-2-3"); -//=> [ '/name/1-2-3', '1-2-3', index: 0 ] +fn("/bar/1-2-3"); +//=> { path: '/name/1-2-3', params: { parts: [ '1', '2', '3' ] } } ``` #### Wildcard -A wildcard can also be used. It is roughly equivalent to `(.*)`. +A wildcard is also supported. It is roughly equivalent to `(.*)`. ```js -const regexp = pathToRegexp("/*"); -// keys = [{ name: '0', pattern: '[^\\/]*', separator: '/', modifier: '*' }] - -regexp.exec("/"); -//=> [ '/', '', index: 0 ] - -regexp.exec("/bar/baz"); -//=> [ '/bar/baz', 'bar/baz', index: 0 ] -``` +const fn = match("/*"); -### Match +fn("/"); +//=> { path: '/', params: {} } -The `match` function returns a function for transforming paths into parameters: - -- **path** A string. -- **options** _(optional)_ The same options as `pathToRegexp`, plus: - - **decode** Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) - -```js -const fn = match("/user/:id"); - -fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } } -fn("/invalid"); //=> false -fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } } +fn("/bar/baz"); +//=> { path: '/bar/baz', params: { '0': [ 'bar', 'baz' ] } } ``` -**Note:** Setting `decode: false` disables the "splitting" behavior of repeated parameters, which is useful if you need the exactly matched parameter back. - ### Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: - **path** A string. -- **options** _(optional)_ Similar to `pathToRegexp` (`delimiter`, `encodePath`, `sensitive`, and `loose`), plus: +- **options** (See [parse](#parse) for more options) + - **sensitive** Regexp will be case sensitive. (default: `false`) - **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`) - **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) @@ -245,14 +212,17 @@ toPathRegexp({ id: "123" }); //=> "/user/123" ## Developers -- If you are rewriting paths with match and compiler, consider using `encode: false` and `decode: false` to keep raw paths passed around. +- If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around. - To ensure matches work on paths containing characters usually encoded, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. -- If matches are intended to be exact, you need to set `loose: false`, `trailing: false`, and `sensitive: true`. -- Enable `strict: true` to detect ReDOS issues. ### Parse -A `parse` function is available and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can passed directly into `pathToRegexp`, `match`, and `compile`. It accepts only two options, `delimiter` and `encodePath`, which makes those options redundant in the above methods. +The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `$match` and `$compile`. + +- **path** A string. +- **options** _(optional)_ + - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) + - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding) ### Tokens @@ -267,14 +237,14 @@ The `tokens` returned by `TokenData` is an array of strings or keys, represented ### Custom path -In some applications, you may not be able to use the `path-to-regexp` syntax (e.g. file-based routing), but you can still use this library for `match`, `compile`, and `pathToRegexp` by building your own `TokenData` instance. For example: +In some applications, you may not be able to use the `path-to-regexp` syntax, but still want to use this library for `match` and `compile`. For example: ```js import { TokenData, match } from "path-to-regexp"; const tokens = ["/", { name: "foo" }]; const path = new TokenData(tokens, "/"); -const fn = match(path); +const fn = $match(path); fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } ``` @@ -299,6 +269,30 @@ Used as a [custom separator](#custom-separator) for repeated parameters. These characters have been reserved for future use. +### Missing separator + +Repeated parameters must have a separator to be valid. For example, `{:foo}*` can't be used. Separators can be defined manually, such as `{:foo;/}*`, or they default to the suffix and prefix with the parameter, such as `{/:foo}*`. + +### Missing parameter name + +Parameter names, the part after `:`, must be a valid JavaScript identifier. For example, it cannot start with a number or dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`. + +### Unterminated quote + +Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character. + +### Pattern cannot start with "?" + +Parameters in `path-to-regexp` must be basic groups. However, you can use features that require the `?` nested within the pattern. For example, `:foo((?!login)[^/]+)` is valid, but `:foo(?!login)` is not. + +### Capturing groups are not allowed + +A parameter pattern can not contain nested capturing groups. + +### Unbalanced or missing pattern + +A parameter pattern must have the expected number of parentheses. An unbalanced amount, such as `((?!login)` implies something has been written that is invalid. Check you didn't forget any parentheses. + ### Express <= 4.x Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways: diff --git a/scripts/redos.ts b/scripts/redos.ts index c675e71..fe2c7ac 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -1,36 +1,28 @@ import { checkSync } from "recheck"; -import { pathToRegexp } from "../src/index.js"; +import { match } from "../src/index.js"; +import { MATCH_TESTS } from "../src/cases.spec.js"; -const TESTS = [ - "/abc{abc:foo}?", - "/:foo{abc:foo}?", - "{:attr1}?{:attr2/}?", - "{:attr1/}?{:attr2/}?", - "{:foo.}?{:bar.}?", - "{:foo([^\\.]+).}?{:bar.}?", - ":foo(a+):bar(b+)", -]; +let safe = 0; +let fail = 0; + +const TESTS = new Set(MATCH_TESTS.map((test) => test.path)); +// const TESTS = [ +// ":path([^\\.]+).:ext", +// ":path.:ext(\\w+)", +// ":path{.:ext([^\\.]+)}", +// "/:path.:ext(\\\\w+)", +// ]; for (const path of TESTS) { - try { - const re = pathToRegexp(path, { strict: true }); - const result = checkSync(re.source, re.flags); - if (result.status === "safe") { - console.log("Safe:", path, String(re)); - } else { - console.log("Fail:", path, String(re)); - } - } catch (err) { - try { - const re = pathToRegexp(path); - const result = checkSync(re.source, re.flags); - if (result.status === "safe") { - console.log("Invalid:", path, String(re)); - } else { - console.log("Pass:", path, String(re)); - } - } catch (err) { - console.log("Error:", path, err.message); - } + const { re } = match(path); + const result = checkSync(re.source, re.flags); + if (result.status === "safe") { + safe++; + console.log("Safe:", path, String(re)); + } else { + fail++; + console.log("Fail:", path, String(re)); } } + +console.log("Safe:", safe, "Fail:", fail); diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 23a1814..a837ea4 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -1,5 +1,4 @@ import type { - Path, MatchOptions, Match, ParseOptions, @@ -16,7 +15,7 @@ export interface ParserTestSet { export interface CompileTestSet { path: string; - options?: CompileOptions; + options?: CompileOptions & ParseOptions; tests: Array<{ input: ParamData | undefined; expected: string | null; @@ -24,8 +23,8 @@ export interface CompileTestSet { } export interface MatchTestSet { - path: Path; - options?: MatchOptions; + path: string; + options?: MatchOptions & ParseOptions; tests: Array<{ input: string; matches: (string | undefined)[] | null; @@ -43,7 +42,7 @@ export const PARSER_TESTS: ParserTestSet[] = [ expected: ["/", { name: "test" }], }, { - path: "/:0", + path: '/:"0"', expected: ["/", { name: "0" }], }, { @@ -54,6 +53,14 @@ export const PARSER_TESTS: ParserTestSet[] = [ path: "/:café", expected: ["/", { name: "café" }], }, + { + path: '/:"123"', + expected: ["/", { name: "123" }], + }, + { + path: '/:"1\\"\\2\\"3"', + expected: ["/", { name: '1"2"3' }], + }, ]; export const COMPILE_TESTS: CompileTestSet[] = [ @@ -82,7 +89,7 @@ export const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "/:0", + path: '/:"0"', tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -206,9 +213,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/"], - expected: { path: "/", index: 0, params: {} }, + expected: { path: "/", params: {} }, }, - { input: "/route", matches: null, expected: false }, + { input: "/route", matches: ["/"], expected: false }, ], }, { @@ -217,14 +224,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test", matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, + expected: { path: "/test", params: {} }, }, { input: "/route", matches: null, expected: false }, - { input: "/test/route", matches: null, expected: false }, + { input: "/test/route", matches: ["/test"], expected: false }, { input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, + matches: ["/test"], + expected: false, }, ], }, @@ -234,14 +241,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/", matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, + expected: { path: "/test/", params: {} }, }, { input: "/route", matches: null, expected: false }, { input: "/test", matches: null, expected: false }, { input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, + matches: ["/test/"], + expected: false, }, ], }, @@ -251,47 +258,36 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, + matches: ["/route", "route"], + expected: false, }, { input: "/route.json", matches: ["/route.json", "route.json"], expected: { path: "/route.json", - index: 0, params: { test: "route.json" }, }, }, { input: "/route.json/", - matches: ["/route.json/", "route.json"], - expected: { - path: "/route.json/", - index: 0, - params: { test: "route.json" }, - }, + matches: ["/route.json", "route.json"], + expected: false, }, { input: "/route/test", - matches: null, + matches: ["/route", "route"], expected: false, }, - { - input: "///route", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, - }, { input: "/caf%C3%A9", matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", - index: 0, params: { test: "café" }, }, }, @@ -300,7 +296,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], expected: { path: "/;,:@&=+$-_.!~*()", - index: 0, params: { test: ";,:@&=+$-_.!~*()" }, }, }, @@ -308,734 +303,102 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, /** - * Case-sensitive paths. - */ - { - path: "/test", - options: { - sensitive: true, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { input: "/TEST", matches: null, expected: false }, - ], - }, - { - path: "/TEST", - options: { - sensitive: true, - }, - tests: [ - { - input: "/TEST", - matches: ["/TEST"], - expected: { path: "/TEST", index: 0, params: {} }, - }, - { input: "/test", matches: null, expected: false }, - ], - }, - - /** - * Non-trailing mode. - */ - { - path: "/test", - options: { - trailing: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/", - matches: null, - expected: false, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test/", - options: { - trailing: false, - }, - tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - ], - }, - { - path: "/:test", - options: { - trailing: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: null, - expected: false, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, - { - input: "///route", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, - }, - ], - }, - { - path: "/:test/", - options: { - trailing: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, - { - input: "/route//", - matches: ["/route//", "route"], - expected: { path: "/route//", index: 0, params: { test: "route" } }, - }, - ], - }, - - /** - * Non-ending mode. - */ - { - path: "/test", - options: { - end: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test////", - matches: ["/test////"], - expected: { path: "/test////", index: 0, params: {} }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/test/route", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test/", - options: { - end: false, - }, - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test", - options: { - end: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route.json", - matches: ["/route.json", "route.json"], - expected: { - path: "/route.json", - index: 0, - params: { test: "route.json" }, - }, - }, - { - input: "/route.json/", - matches: ["/route.json/", "route.json"], - expected: { - path: "/route.json/", - index: 0, - params: { test: "route.json" }, - }, - }, - { - input: "/route/test", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route.json/test", - matches: ["/route.json", "route.json"], - expected: { - path: "/route.json", - index: 0, - params: { test: "route.json" }, - }, - }, - { - input: "///route///test", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, - }, - { - input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], - expected: { - path: "/caf%C3%A9", - index: 0, - params: { test: "café" }, - }, - }, - ], - }, - { - path: "/:test/", - options: { - end: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, - { - input: "/route//test", - matches: null, - expected: false, - }, - ], - }, - { - path: "", - options: { - end: false, - }, - tests: [ - { - input: "", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "route", - matches: null, - expected: false, - }, - { - input: "/route", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/route/", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - ], - }, - - /** - * Non-starting mode. - */ - { - path: "/test", - options: { - start: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/route/test", - matches: ["/test"], - expected: { path: "/test", index: 6, params: {} }, - }, - { - input: "/route/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 6, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - { - input: "/route", - matches: null, - expected: false, - }, - ], - }, - { - path: "/test/", - options: { - start: false, - }, - tests: [ - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 6, params: {} }, - }, - { - input: "/route/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 6, params: {} }, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test", - options: { - start: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: ["/test", "test"], - expected: { path: "/test", index: 6, params: { test: "test" } }, - }, - { - input: "/route/test/", - matches: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, - }, - ], - }, - { - path: "/:test/", - options: { - start: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, - }, - { - input: "/route/test//", - matches: ["/test//", "test"], - expected: { path: "/test//", index: 6, params: { test: "test" } }, - }, - ], - }, - { - path: "", - options: { - start: false, - }, - tests: [ - { - input: "", - matches: [""], - expected: { path: "", index: 0, params: {} }, - }, - { - input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, - }, - { - input: "route", - matches: [""], - expected: { path: "", index: 5, params: {} }, - }, - { - input: "/route", - matches: [""], - expected: { path: "", index: 6, params: {} }, - }, - { - input: "/route/", - matches: ["/"], - expected: { path: "/", index: 6, params: {} }, - }, - ], - }, - - /** - * Non-ending and non-trailing modes. - */ - { - path: "/test", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - ], - }, - { - path: "/test/", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, - { - input: "/test", - matches: null, - expected: false, - }, - { - input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, - }, - { - input: "/test/route", - matches: null, - expected: false, - }, - { - input: "/route/test/deep", - matches: null, - expected: false, - }, - ], - }, - { - path: "/:test", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test/", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, - }, - ], - }, - { - path: "/:test/", - options: { - end: false, - trailing: false, - }, - tests: [ - { - input: "/route", - matches: null, - expected: false, - }, - { - input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, - }, - { - input: "/route/test", - matches: null, - expected: false, - }, - { - input: "/route/test/", - matches: null, - expected: false, - }, + * Case-sensitive paths. + */ + { + path: "/test", + options: { + sensitive: true, + }, + tests: [ { - input: "/route/test//", - matches: null, - expected: false, + input: "/test", + matches: ["/test"], + expected: { path: "/test", params: {} }, }, + { input: "/TEST", matches: null, expected: false }, + ], + }, + { + path: "/TEST", + options: { + sensitive: true, + }, + tests: [ { - input: "/route//test", - matches: null, - expected: false, + input: "/TEST", + matches: ["/TEST"], + expected: { path: "/TEST", params: {} }, }, + { input: "/test", matches: null, expected: false }, ], }, /** - * Non-starting and non-ending modes. + * Non-ending mode. */ { path: "/test", options: { - start: false, end: false, }, tests: [ { input: "/test", matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, + expected: { path: "/test", params: {} }, }, { input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, + matches: ["/test"], + expected: { path: "/test", params: {} }, }, { - input: "/test/route", + input: "/test////", matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, + expected: { path: "/test", params: {} }, }, { input: "/route/test", + matches: null, + expected: false, + }, + { + input: "/test/route", matches: ["/test"], - expected: { path: "/test", index: 6, params: {} }, + expected: { path: "/test", params: {} }, + }, + { + input: "/route", + matches: null, + expected: false, }, ], }, { path: "/test/", options: { - start: false, end: false, }, tests: [ - { - input: "/test/", - matches: ["/test/"], - expected: { path: "/test/", index: 0, params: {} }, - }, { input: "/test", matches: null, expected: false, }, + { + input: "/test/", + matches: ["/test/"], + expected: { path: "/test/", params: {} }, + }, { input: "/test//", - matches: ["/test//"], - expected: { path: "/test//", index: 0, params: {} }, + matches: ["/test/"], + expected: { path: "/test/", params: {} }, }, { input: "/test/route", - matches: null, + matches: ["/test/"], expected: false, }, { @@ -1043,46 +406,66 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: null, expected: false, }, - { - input: "/route/test//deep", - matches: null, - expected: false, - }, ], }, { path: "/:test", options: { - start: false, end: false, }, tests: [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, + matches: ["/route", "route"], + expected: { path: "/route", params: { test: "route" } }, + }, + { + input: "/route.json", + matches: ["/route.json", "route.json"], + expected: { + path: "/route.json", + params: { test: "route.json" }, + }, + }, + { + input: "/route.json/", + matches: ["/route.json", "route.json"], + expected: { + path: "/route.json", + params: { test: "route.json" }, + }, }, { input: "/route/test", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { - input: "/route/test/", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + input: "/route.json/test", + matches: ["/route.json", "route.json"], + expected: { + path: "/route.json", + params: { test: "route.json" }, + }, + }, + { + input: "/caf%C3%A9", + matches: ["/caf%C3%A9", "caf%C3%A9"], + expected: { + path: "/caf%C3%A9", + params: { test: "café" }, + }, }, ], }, { path: "/:test/", options: { - start: false, end: false, }, tests: [ @@ -1094,77 +477,80 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route/", matches: ["/route/", "route"], - expected: { path: "/route/", index: 0, params: { test: "route" } }, + expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route/test", - matches: null, + matches: ["/route/", "route"], expected: false, }, { input: "/route/test/", - matches: ["/test/", "test"], - expected: { path: "/test/", index: 6, params: { test: "test" } }, + matches: ["/route/", "route"], + expected: false, }, { - input: "/route/test//", - matches: ["/test//", "test"], - expected: { path: "/test//", index: 6, params: { test: "test" } }, + input: "/route//test", + matches: ["/route/", "route"], + expected: { path: "/route/", params: { test: "route" } }, }, ], }, - - /** - * Optional. - */ { - path: "{/:test}?", + path: "", + options: { + end: false, + }, tests: [ { - input: "/route", - matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + input: "", + matches: [""], + expected: { path: "", params: {} }, }, { - input: "///route", - matches: ["///route", "route"], - expected: { path: "///route", index: 0, params: { test: "route" } }, + input: "/", + matches: [""], + expected: { path: "", params: {} }, }, { - input: "///route///", - matches: ["///route///", "route"], - expected: { path: "///route///", index: 0, params: { test: "route" } }, + input: "route", + matches: [""], + expected: false, }, { - input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, + input: "/route", + matches: [""], + expected: { path: "", params: {} }, }, { - input: "///", - matches: ["///", undefined], - expected: { path: "///", index: 0, params: {} }, + input: "/route/", + matches: [""], + expected: { path: "", params: {} }, }, ], }, + + /** + * Optional. + */ { path: "{/:test}?", - options: { - trailing: false, - }, tests: [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { - input: "/route/", - matches: null, + input: "", + matches: ["", undefined], + expected: { path: "", params: {} }, + }, + { + input: "/", + matches: ["", undefined], expected: false, }, - { input: "/", matches: null, expected: false }, - { input: "///", matches: null, expected: false }, ], }, { @@ -1173,22 +559,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/bar", matches: ["/bar", undefined], - expected: { path: "/bar", index: 0, params: {} }, + expected: { path: "/bar", params: {} }, }, { input: "/foo/bar", matches: ["/foo/bar", "foo"], - expected: { path: "/foo/bar", index: 0, params: { test: "foo" } }, - }, - { - input: "///foo///bar", - matches: ["///foo///bar", "foo"], - expected: { path: "///foo///bar", index: 0, params: { test: "foo" } }, + expected: { path: "/foo/bar", params: { test: "foo" } }, }, { input: "/foo/bar/", - matches: ["/foo/bar/", "foo"], - expected: { path: "/foo/bar/", index: 0, params: { test: "foo" } }, + matches: ["/foo/bar", "foo"], + expected: false, }, ], }, @@ -1198,17 +579,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "-bar", matches: ["-bar", undefined], - expected: { path: "-bar", index: 0, params: {} }, + expected: { path: "-bar", params: {} }, }, { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: "foo" } }, + expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar/", "foo"], - expected: { path: "/foo-bar/", index: 0, params: { test: "foo" } }, + matches: ["/foo-bar", "foo"], + expected: false, }, ], }, @@ -1218,17 +599,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/-bar", matches: ["/-bar", undefined], - expected: { path: "/-bar", index: 0, params: {} }, + expected: { path: "/-bar", params: {} }, }, { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: "foo" } }, + expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar/", "foo"], - expected: { path: "/foo-bar/", index: 0, params: { test: "foo" } }, + matches: ["/foo-bar", "foo"], + expected: false, }, ], }, @@ -1241,34 +622,24 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: {} }, + matches: ["", undefined], + expected: false, }, { input: "//", - matches: ["//", undefined], - expected: { path: "//", index: 0, params: {} }, + matches: ["", undefined], + expected: false, }, { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: ["route"] } }, + expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", - index: 0, - params: { test: ["some", "basic", "route"] }, - }, - }, - { - input: "///some///basic///route", - matches: ["///some///basic///route", "some///basic///route"], - expected: { - path: "///some///basic///route", - index: 0, params: { test: ["some", "basic", "route"] }, }, }, @@ -1280,7 +651,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "-bar", matches: ["-bar", undefined], - expected: { path: "-bar", index: 0, params: {} }, + expected: { path: "-bar", params: {} }, }, { input: "/-bar", @@ -1290,14 +661,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", - index: 0, params: { test: ["foo", "baz"] }, }, }, @@ -1323,23 +693,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: ["route"] } }, + expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", - index: 0, - params: { test: ["some", "basic", "route"] }, - }, - }, - { - input: "///some///basic///route", - matches: ["///some///basic///route", "some///basic///route"], - expected: { - path: "///some///basic///route", - index: 0, params: { test: ["some", "basic", "route"] }, }, }, @@ -1361,14 +721,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo-bar", matches: ["/foo-bar", "foo"], - expected: { path: "/foo-bar", index: 0, params: { test: ["foo"] } }, + expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", - index: 0, params: { test: ["foo", "baz"] }, }, }, @@ -1384,7 +743,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { test: "123" } }, + expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", @@ -1393,7 +752,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/123/abc", - matches: null, + matches: ["/123", "123"], expected: false, }, ], @@ -1419,7 +778,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123-bar", matches: ["/123-bar", "123"], - expected: { path: "/123-bar", index: 0, params: { test: "123" } }, + expected: { path: "/123-bar", params: { test: "123" } }, }, { input: "/123/456-bar", @@ -1434,19 +793,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", ""], - expected: { path: "/", index: 0, params: { test: "" } }, + expected: { path: "/", params: { test: "" } }, }, { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/123", matches: ["/route/123", "route/123"], expected: { path: "/route/123", - index: 0, params: { test: "route/123" }, }, }, @@ -1455,7 +813,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], expected: { path: "/;,:@&=/+$-_.!/~*()", - index: 0, params: { test: ";,:@&=/+$-_.!/~*()" }, }, }, @@ -1467,7 +824,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/abc", matches: ["/abc", "abc"], - expected: { path: "/abc", index: 0, params: { test: "abc" } }, + expected: { path: "/abc", params: { test: "abc" } }, }, { input: "/123", @@ -1476,7 +833,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abc/123", - matches: null, + matches: ["/abc", "abc"], expected: false, }, ], @@ -1487,12 +844,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/this", matches: ["/this", "this"], - expected: { path: "/this", index: 0, params: { test: "this" } }, + expected: { path: "/this", params: { test: "this" } }, }, { input: "/that", matches: ["/that", "that"], - expected: { path: "/that", index: 0, params: { test: "that" } }, + expected: { path: "/that", params: { test: "that" } }, }, { input: "/foo", @@ -1506,20 +863,19 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { test: undefined } }, + matches: ["", undefined], + expected: false, }, { input: "/abc", matches: ["/abc", "abc"], - expected: { path: "/abc", index: 0, params: { test: ["abc"] } }, + expected: { path: "/abc", params: { test: ["abc"] } }, }, { input: "/abc/abc", matches: ["/abc/abc", "abc/abc"], expected: { path: "/abc/abc", - index: 0, params: { test: ["abc", "abc"] }, }, }, @@ -1528,7 +884,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/xyz/xyz", "xyz/xyz"], expected: { path: "/xyz/xyz", - index: 0, params: { test: ["xyz", "xyz"] }, }, }, @@ -1537,7 +892,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/abc/xyz", "abc/xyz"], expected: { path: "/abc/xyz", - index: 0, params: { test: ["abc", "xyz"] }, }, }, @@ -1546,13 +900,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/abc/xyz/abc/xyz", "abc/xyz/abc/xyz"], expected: { path: "/abc/xyz/abc/xyz", - index: 0, params: { test: ["abc", "xyz", "abc", "xyz"] }, }, }, { input: "/xyzxyz", - matches: null, + matches: ["/xyz", "xyz"], expected: false, }, ], @@ -1567,7 +920,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "test", matches: ["test"], - expected: { path: "test", index: 0, params: {} }, + expected: { path: "test", params: {} }, }, { input: "/test", @@ -1582,7 +935,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "route", matches: ["route", "route"], - expected: { path: "route", index: 0, params: { test: "route" } }, + expected: { path: "route", params: { test: "route" } }, }, { input: "/route", @@ -1591,8 +944,8 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "route/", - matches: ["route/", "route"], - expected: { path: "route/", index: 0, params: { test: "route" } }, + matches: ["route", "route"], + expected: false, }, ], }, @@ -1602,12 +955,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "test", matches: ["test", "test"], - expected: { path: "test", index: 0, params: { test: "test" } }, + expected: { path: "test", params: { test: "test" } }, }, { input: "", matches: ["", undefined], - expected: { path: "", index: 0, params: {} }, + expected: { path: "", params: {} }, }, ], }, @@ -1617,7 +970,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "route/", matches: ["route/", "route"], - expected: { path: "route/", index: 0, params: { test: ["route"] } }, + expected: { path: "route/", params: { test: ["route"] } }, }, { input: "/route", @@ -1634,7 +987,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["foo/bar/", "foo/bar"], expected: { path: "foo/bar/", - index: 0, params: { test: ["foo", "bar"] }, }, }, @@ -1650,7 +1002,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test.json", matches: ["/test.json"], - expected: { path: "/test.json", index: 0, params: {} }, + expected: { path: "/test.json", params: {} }, }, { input: "/test", @@ -1670,19 +1022,28 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test.json", matches: ["/test.json", "test"], - expected: { path: "/test.json", index: 0, params: { test: "test" } }, + expected: { path: "/test.json", params: { test: "test" } }, }, { input: "/route.json", matches: ["/route.json", "route"], - expected: { path: "/route.json", index: 0, params: { test: "route" } }, + expected: { path: "/route.json", params: { test: "route" } }, + }, + { + input: "/route.json.json", + matches: ["/route.json", "route"], + expected: false, }, + ], + }, + { + path: "/:test([^/]+).json", + tests: [ { input: "/route.json.json", matches: ["/route.json.json", "route.json"], expected: { path: "/route.json.json", - index: 0, params: { test: "route.json" }, }, }, @@ -1698,7 +1059,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test.html", matches: ["/test.html", "html"], - expected: { path: "/test.html", index: 0, params: { format: "html" } }, + expected: { path: "/test.html", params: { format: "html" } }, }, { input: "/test", @@ -1715,7 +1076,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html.json", "html", "json"], expected: { path: "/test.html.json", - index: 0, params: { format: "json" }, }, }, @@ -1732,12 +1092,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test", matches: ["/test", undefined], - expected: { path: "/test", index: 0, params: { format: undefined } }, + expected: { path: "/test", params: { format: undefined } }, }, { input: "/test.html", matches: ["/test.html", "html"], - expected: { path: "/test.html", index: 0, params: { format: "html" } }, + expected: { path: "/test.html", params: { format: "html" } }, }, ], }, @@ -1754,7 +1114,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html", "html"], expected: { path: "/test.html", - index: 0, params: { format: ["html"] }, }, }, @@ -1763,7 +1122,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html.json", "html.json"], expected: { path: "/test.html.json", - index: 0, params: { format: ["html", "json"] }, }, }, @@ -1782,7 +1140,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.html", "html"], expected: { path: "/test.html", - index: 0, params: { format: ["html"] }, }, }, @@ -1791,7 +1148,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/test.hbs.html", "hbs.html"], expected: { path: "/test.hbs.html", - index: 0, params: { format: ["hbs", "html"] }, }, }, @@ -1809,7 +1165,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.html", "route", "html"], expected: { path: "/route.html", - index: 0, params: { test: "route", format: "html" }, }, }, @@ -1823,7 +1178,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.html.json", "route", "html.json"], expected: { path: "/route.html.json", - index: 0, params: { test: "route", format: "html.json" }, }, }, @@ -1835,14 +1189,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route", undefined], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json", matches: ["/route.json", "route", "json"], expected: { path: "/route.json", - index: 0, params: { test: "route", format: "json" }, }, }, @@ -1851,7 +1204,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.json.html", "route", "json.html"], expected: { path: "/route.json.html", - index: 0, params: { test: "route", format: "json.html" }, }, }, @@ -1865,7 +1217,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route.htmlz", "route", "html"], expected: { path: "/route.htmlz", - index: 0, params: { test: "route", format: "html" }, }, }, @@ -1886,7 +1237,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, + expected: { path: "/123", params: { "0": "123" } }, }, { input: "/abc", @@ -1895,7 +1246,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/123/abc", - matches: null, + matches: ["/123", "123"], expected: false, }, ], @@ -1905,13 +1256,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, + matches: ["", undefined], + expected: false, }, { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { "0": "123" } }, + expected: { path: "/123", params: { "0": "123" } }, }, ], }, @@ -1923,7 +1274,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route(\\123\\)", "123\\"], expected: { path: "/route(\\123\\)", - index: 0, params: { "0": "123\\" }, }, }, @@ -1940,22 +1290,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "", matches: [""], - expected: { path: "", index: 0, params: {} }, + expected: { path: "", params: {} }, }, { input: "/", - matches: ["/"], - expected: { path: "/", index: 0, params: {} }, + matches: [""], + expected: false, }, { input: "/foo", - matches: null, + matches: [""], expected: false, }, { input: "/route", matches: ["/route"], - expected: { path: "/route", index: 0, params: {} }, + expected: { path: "/route", params: {} }, }, ], }, @@ -1965,12 +1315,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", ""], - expected: { path: "/", index: 0, params: { "0": "" } }, + expected: { path: "/", params: { "0": "" } }, }, { input: "/login", matches: ["/login", "login"], - expected: { path: "/login", index: 0, params: { "0": "login" } }, + expected: { path: "/login", params: { "0": "login" } }, }, ], }, @@ -1989,7 +1339,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/(testing)", matches: ["/(testing)"], - expected: { path: "/(testing)", index: 0, params: {} }, + expected: { path: "/(testing)", params: {} }, }, ], }, @@ -1999,7 +1349,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/.+*?{}=^!:$[]|", matches: ["/.+*?{}=^!:$[]|"], - expected: { path: "/.+*?{}=^!:$[]|", index: 0, params: {} }, + expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, ], }, @@ -2009,12 +1359,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/u123", matches: ["/test/u123", "u123", undefined], - expected: { path: "/test/u123", index: 0, params: { uid: "u123" } }, + expected: { path: "/test/u123", params: { uid: "u123" } }, }, { input: "/test/c123", matches: ["/test/c123", undefined, "c123"], - expected: { path: "/test/c123", index: 0, params: { cid: "c123" } }, + expected: { path: "/test/c123", params: { cid: "c123" } }, }, ], }, @@ -2028,14 +1378,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/icon-240.png", matches: ["/icon-240.png", "240"], - expected: { path: "/icon-240.png", index: 0, params: { res: "240" } }, + expected: { path: "/icon-240.png", params: { res: "240" } }, }, { input: "/apple-icon-240.png", matches: ["/apple-icon-240.png", "240"], expected: { path: "/apple-icon-240.png", - index: 0, params: { res: "240" }, }, }, @@ -2053,7 +1402,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/match/route", "match", "route"], expected: { path: "/match/route", - index: 0, params: { foo: "match", bar: "route" }, }, }, @@ -2065,7 +1413,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo(test)/bar", matches: ["/foo(test)/bar", "foo"], - expected: { path: "/foo(test)/bar", index: 0, params: { foo: "foo" } }, + expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, }, { input: "/foo/bar", @@ -2082,7 +1430,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/endpoint/user", "endpoint", "user"], expected: { path: "/endpoint/user", - index: 0, params: { remote: "endpoint", user: "user" }, }, }, @@ -2091,7 +1438,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/endpoint/user-name", "endpoint", "user-name"], expected: { path: "/endpoint/user-name", - index: 0, params: { remote: "endpoint", user: "user-name" }, }, }, @@ -2100,7 +1446,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/foo.bar/user-name", "foo.bar", "user-name"], expected: { path: "/foo.bar/user-name", - index: 0, params: { remote: "foo.bar", user: "user-name" }, }, }, @@ -2112,7 +1457,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route?", matches: ["/route?", "route"], - expected: { path: "/route?", index: 0, params: { foo: "route" } }, + expected: { path: "/route?", params: { foo: "route" } }, }, { input: "/route", @@ -2127,7 +1472,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foobar", matches: ["/foobar", "foo"], - expected: { path: "/foobar", index: 0, params: { foo: ["foo"] } }, + expected: { path: "/foobar", params: { foo: ["foo"] } }, }, { input: "/foo/bar", @@ -2136,12 +1481,8 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/barbar", - matches: ["/foo/barbar", "foo/bar"], - expected: { - path: "/foo/barbar", - index: 0, - params: { foo: ["foo", "bar"] }, - }, + matches: null, + expected: false, }, ], }, @@ -2151,12 +1492,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foobaz", matches: ["/foobaz", "foo"], - expected: { path: "/foobaz", index: 0, params: { pre: "foo" } }, + expected: { path: "/foobaz", params: { pre: "foo" } }, }, { input: "/baz", matches: ["/baz", undefined], - expected: { path: "/baz", index: 0, params: { pre: undefined } }, + expected: { path: "/baz", params: { pre: undefined } }, }, ], }, @@ -2168,7 +1509,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -2187,7 +1527,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -2196,7 +1535,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello()", "hello", undefined], expected: { path: "/hello()", - index: 0, params: { foo: "hello", bar: undefined }, }, }, @@ -2208,20 +1546,19 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/video", matches: ["/video", "video", undefined], - expected: { path: "/video", index: 0, params: { postType: "video" } }, + expected: { path: "/video", params: { postType: "video" } }, }, { input: "/video+test", matches: ["/video+test", "video", "+test"], expected: { path: "/video+test", - index: 0, params: { 0: "+test", postType: "video" }, }, }, { input: "/video+", - matches: null, + matches: ["/video", "video", undefined], expected: false, }, ], @@ -2239,21 +1576,19 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["-ext", undefined, undefined], expected: { path: "-ext", - index: 0, params: { foo: undefined, bar: undefined }, }, }, { input: "/foo-ext", matches: ["/foo-ext", "foo", undefined], - expected: { path: "/foo-ext", index: 0, params: { foo: "foo" } }, + expected: { path: "/foo-ext", params: { foo: "foo" } }, }, { input: "/foo/bar-ext", matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", - index: 0, params: { foo: "foo", bar: "bar" }, }, }, @@ -2270,14 +1605,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/foo-ext", matches: ["/foo-ext", "foo", undefined], - expected: { path: "/foo-ext", index: 0, params: { required: "foo" } }, + expected: { path: "/foo-ext", params: { required: "foo" } }, }, { input: "/foo/bar-ext", matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", - index: 0, params: { required: "foo", optional: "bar" }, }, }, @@ -2298,7 +1632,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/café", matches: ["/café", "café"], - expected: { path: "/café", index: 0, params: { foo: "café" } }, + expected: { path: "/café", params: { foo: "café" } }, }, ], }, @@ -2313,7 +1647,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", - index: 0, params: { foo: "caf%C3%A9" }, }, }, @@ -2325,7 +1658,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/café", matches: ["/café"], - expected: { path: "/café", index: 0, params: {} }, + expected: { path: "/café", params: {} }, }, ], }, @@ -2338,7 +1671,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/caf%C3%A9", matches: ["/caf%C3%A9"], - expected: { path: "/caf%C3%A9", index: 0, params: {} }, + expected: { path: "/caf%C3%A9", params: {} }, }, ], }, @@ -2357,7 +1690,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["example.com", "example"], expected: { path: "example.com", - index: 0, params: { domain: "example" }, }, }, @@ -2366,7 +1698,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["github.com", "github"], expected: { path: "github.com", - index: 0, params: { domain: "github" }, }, }, @@ -2383,7 +1714,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", - index: 0, params: { domain: "example" }, }, }, @@ -2392,7 +1722,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", - index: 0, params: { domain: "github" }, }, }, @@ -2407,14 +1736,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "mail.com", matches: ["mail.com", undefined], - expected: { path: "mail.com", index: 0, params: { domain: undefined } }, + expected: { path: "mail.com", params: { domain: undefined } }, }, { input: "mail.example.com", matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", - index: 0, params: { domain: "example" }, }, }, @@ -2423,7 +1751,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", - index: 0, params: { domain: "github" }, }, }, @@ -2438,12 +1765,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "example.com", matches: ["example.com", "com"], - expected: { path: "example.com", index: 0, params: { ext: "com" } }, + expected: { path: "example.com", params: { ext: "com" } }, }, { input: "example.org", matches: ["example.org", "org"], - expected: { path: "example.org", index: 0, params: { ext: "org" } }, + expected: { path: "example.org", params: { ext: "org" } }, }, ], }, @@ -2457,11 +1784,11 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "this is a test", matches: ["this is"], - expected: { path: "this is", index: 0, params: {} }, + expected: { path: "this is", params: {} }, }, { input: "this isn't", - matches: null, + matches: ["this is"], expected: false, }, ], @@ -2476,12 +1803,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "$x", matches: ["$x", "x", undefined], - expected: { path: "$x", index: 0, params: { foo: "x" } }, + expected: { path: "$x", params: { foo: "x" } }, }, { input: "$x$y", matches: ["$x$y", "x", "y"], - expected: { path: "$x$y", index: 0, params: { foo: "x", bar: "y" } }, + expected: { path: "$x$y", params: { foo: "x", bar: "y" } }, }, ], }, @@ -2491,12 +1818,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "$x", matches: ["$x", "x"], - expected: { path: "$x", index: 0, params: { foo: ["x"] } }, + expected: { path: "$x", params: { foo: ["x"] } }, }, { input: "$x$y", matches: ["$x$y", "x$y"], - expected: { path: "$x$y", index: 0, params: { foo: ["x", "y"] } }, + expected: { path: "$x$y", params: { foo: ["x", "y"] } }, }, ], }, @@ -2506,14 +1833,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "name", matches: ["name", undefined, undefined, undefined], - expected: { path: "name", index: 0, params: {} }, + expected: { path: "name", params: {} }, }, { input: "name/test", matches: ["name/test", "test", undefined, undefined], expected: { path: "name/test", - index: 0, params: { attr1: "test" }, }, }, @@ -2522,7 +1848,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1", "1", undefined, undefined], expected: { path: "name/1", - index: 0, params: { attr1: "1" }, }, }, @@ -2531,7 +1856,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2", "1", "2", undefined], expected: { path: "name/1-2", - index: 0, params: { attr1: "1", attr2: "2" }, }, }, @@ -2540,18 +1864,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2-3", "1", "2", "3"], expected: { path: "name/1-2-3", - index: 0, params: { attr1: "1", attr2: "2", attr3: "3" }, }, }, { input: "name/foo-bar/route", - matches: null, + matches: ["name/foo-bar", "foo", "bar", undefined], expected: false, }, { input: "name/test/route", - matches: null, + matches: ["name/test", "test", undefined, undefined], expected: false, }, ], @@ -2562,14 +1885,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "name", matches: ["name", undefined], - expected: { path: "name", index: 0, params: {} }, + expected: { path: "name", params: {} }, }, { input: "name/1", matches: ["name/1", "1"], expected: { path: "name/1", - index: 0, params: { attrs: ["1"] }, }, }, @@ -2578,7 +1900,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2", "1-2"], expected: { path: "name/1-2", - index: 0, params: { attrs: ["1", "2"] }, }, }, @@ -2587,18 +1908,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["name/1-2-3", "1-2-3"], expected: { path: "name/1-2-3", - index: 0, params: { attrs: ["1", "2", "3"] }, }, }, { input: "name/foo-bar/route", - matches: null, + matches: ["name/foo-bar", "foo-bar"], expected: false, }, { input: "name/test/route", - matches: null, + matches: ["name/test", "test"], expected: false, }, ], @@ -2613,7 +1933,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/123", matches: ["/123", "123"], - expected: { path: "/123", index: 0, params: { test: "123" } }, + expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", @@ -2622,17 +1942,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/123/abc", - matches: null, + matches: ["/123", "123"], expected: false, }, { input: "/123.123", matches: ["/123.123", "123.123"], - expected: { path: "/123.123", index: 0, params: { test: "123.123" } }, + expected: { path: "/123.123", params: { test: "123.123" } }, }, { input: "/123.abc", - matches: null, + matches: ["/123", "123"], expected: false, }, ], @@ -2643,7 +1963,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { test: "route" } }, + expected: { path: "/route", params: { test: "route" } }, }, { input: "/login", @@ -2662,14 +1982,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/user/123", matches: ["/user/123", undefined, "123"], - expected: { path: "/user/123", index: 0, params: { user: "123" } }, + expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", matches: ["/users/123", "s", "123"], expected: { path: "/users/123", - index: 0, params: { 0: "s", user: "123" }, }, }, @@ -2681,12 +2000,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/user/123", matches: ["/user/123", "123"], - expected: { path: "/user/123", index: 0, params: { user: "123" } }, + expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", matches: ["/users/123", "123"], - expected: { path: "/users/123", index: 0, params: { user: "123" } }, + expected: { path: "/users/123", params: { user: "123" } }, }, ], }, @@ -2702,7 +2021,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", - index: 0, params: { path: ["hello", "world"], ext: ["txt"] }, }, }, @@ -2711,18 +2029,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/files/hello/world.txt.png", "hello/world", "txt.png"], expected: { path: "/files/hello/world.txt.png", - index: 0, params: { path: ["hello", "world"], ext: ["txt", "png"] }, }, }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg/gif", "my/photo.jpg/gif", undefined], - expected: { - path: "/files/my/photo.jpg/gif", - index: 0, - params: { path: ["my", "photo.jpg", "gif"], ext: undefined }, - }, + matches: ["/files/my/photo.jpg", "my/photo", "jpg"], + expected: false, }, ], }, @@ -2734,18 +2047,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", - index: 0, params: { path: ["hello", "world"], ext: "txt" }, }, }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg/gif", "my/photo.jpg/gif", undefined], - expected: { - path: "/files/my/photo.jpg/gif", - index: 0, - params: { path: ["my", "photo.jpg", "gif"], ext: undefined }, - }, + matches: ["/files/my/photo.jpg", "my/photo", "jpg"], + expected: false, }, ], }, @@ -2755,7 +2063,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "#/", matches: ["#/", undefined], - expected: { path: "#/", index: 0, params: {} }, + expected: { path: "#/", params: {} }, }, ], }, @@ -2763,11 +2071,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ path: "/foo{/:bar}*", tests: [ { - input: "/foo/test1//test2", - matches: ["/foo/test1//test2", "test1//test2"], + input: "/foo/test1/test2", + matches: ["/foo/test1/test2", "test1/test2"], expected: { - path: "/foo/test1//test2", - index: 0, + path: "/foo/test1/test2", params: { bar: ["test1", "test2"] }, }, }, @@ -2784,7 +2091,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/entity/foo/", matches: ["/entity/foo/", "foo", undefined], - expected: { path: "/entity/foo/", index: 0, params: { id: "foo" } }, + expected: { path: "/entity/foo/", params: { id: "foo" } }, }, ], }, @@ -2799,19 +2106,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/", matches: ["/test/", undefined], - expected: { path: "/test/", index: 0, params: {} }, + expected: { path: "/test/", params: {} }, }, { input: "/test/route", matches: ["/test/route", "route"], - expected: { path: "/test/route", index: 0, params: { "0": ["route"] } }, + expected: { path: "/test/route", params: { "0": ["route"] } }, }, { input: "/test/route/nested", matches: ["/test/route/nested", "route/nested"], expected: { path: "/test/route/nested", - index: 0, params: { "0": ["route", "nested"] }, }, }, @@ -2827,19 +2133,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", undefined], - expected: { path: "/", index: 0, params: { "0": undefined } }, + expected: { path: "/", params: { "0": undefined } }, }, { input: "/route", matches: ["/route", "route"], - expected: { path: "/route", index: 0, params: { "0": ["route"] } }, + expected: { path: "/route", params: { "0": ["route"] } }, }, { input: "/route/nested", matches: ["/route/nested", "route/nested"], expected: { path: "/route/nested", - index: 0, params: { "0": ["route", "nested"] }, }, }, @@ -2851,12 +2156,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", "/"], - expected: { path: "/", index: 0, params: { "0": ["", ""] } }, + expected: { path: "/", params: { "0": ["", ""] } }, }, { input: "/test", matches: ["/test", "/test"], - expected: { path: "/test", index: 0, params: { "0": ["", "test"] } }, + expected: { path: "/test", params: { "0": ["", "test"] } }, }, ], }, @@ -2867,32 +2172,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", "/"], - expected: { path: "/", index: 0, params: { "0": "/" } }, + expected: { path: "/", params: { "0": "/" } }, }, { input: "/test", matches: ["/test", "/test"], - expected: { path: "/test", index: 0, params: { "0": "/test" } }, - }, - ], - }, - - /** - * No loose. - */ - { - path: "/test", - options: { loose: false }, - tests: [ - { - input: "/test", - matches: ["/test"], - expected: { path: "/test", index: 0, params: {} }, - }, - { - input: "//test", - matches: null, - expected: false, + expected: { path: "/test", params: { "0": "/test" } }, }, ], }, @@ -2906,14 +2191,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route", matches: ["/route", "route", undefined], - expected: { path: "/route", index: 0, params: { foo: "route" } }, + expected: { path: "/route", params: { foo: "route" } }, }, { input: "/route/test/again", matches: ["/route/test/again", "route", "again"], expected: { path: "/route/test/again", - index: 0, params: { foo: "route", bar: "again" }, }, }, @@ -2929,14 +2213,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/", matches: ["/", "test"], - expected: { path: "/", index: 0, params: { foo: ["test"] } }, + expected: { path: "/", params: { foo: ["test"] } }, }, { input: "/", matches: ["/", "test>", - index: 0, params: { foo: ["test", "again"] }, }, }, @@ -2952,21 +2235,20 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "", matches: ["", undefined, undefined], - expected: { path: "", index: 0, params: {} }, + expected: { path: "", params: {} }, }, { input: "test/", matches: ["test/", "test", undefined], expected: { path: "test/", - index: 0, params: { foo: "test" }, }, }, { input: "a/b.", matches: ["a/b.", "a", "b"], - expected: { path: "a/b.", index: 0, params: { foo: "a", bar: "b" } }, + expected: { path: "a/b.", params: { foo: "a", bar: "b" } }, }, ], }, @@ -2976,31 +2258,30 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/abc", matches: ["/abc", undefined], - expected: { path: "/abc", index: 0, params: {} }, + expected: { path: "/abc", params: {} }, }, { input: "/abcabc", - matches: null, + matches: ["/abc", undefined], expected: false, }, { input: "/abcabc123", matches: ["/abcabc123", "123"], - expected: { path: "/abcabc123", index: 0, params: { foo: "123" } }, + expected: { path: "/abcabc123", params: { foo: "123" } }, }, { input: "/abcabcabc123", matches: ["/abcabcabc123", "abc123"], expected: { path: "/abcabcabc123", - index: 0, params: { foo: "abc123" }, }, }, { input: "/abcabcabc", matches: ["/abcabcabc", "abc"], - expected: { path: "/abcabcabc", index: 0, params: { foo: "abc" } }, + expected: { path: "/abcabcabc", params: { foo: "abc" } }, }, ], }, @@ -3009,39 +2290,33 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", "abc", undefined], - expected: { path: "/abc", index: 0, params: { foo: "abc" } }, + matches: null, + expected: false, }, { input: "/abcabc", - matches: ["/abcabc", "abcabc", undefined], - expected: { path: "/abcabc", index: 0, params: { foo: "abcabc" } }, + matches: null, + expected: false, }, { input: "/abcabc123", - matches: ["/abcabc123", "abc", "123"], - expected: { - path: "/abcabc123", - index: 0, - params: { foo: "abc", bar: "123" }, - }, + matches: null, + expected: false, }, { - input: "/abcabcabc123", - matches: ["/abcabcabc123", "abc", "abc123"], + input: "/acb", + matches: ["/acb", "acb", undefined], expected: { - path: "/abcabcabc123", - index: 0, - params: { foo: "abc", bar: "abc123" }, + path: "/acb", + params: { foo: "acb" }, }, }, { - input: "/abcabcabc", - matches: ["/abcabcabc", "abc", "abc"], + input: "/acbabc123", + matches: ["/acbabc123", "acb", "123"], expected: { - path: "/abcabcabc", - index: 0, - params: { foo: "abc", bar: "abc" }, + path: "/acbabc123", + params: { foo: "acb", bar: "123" }, }, }, ], @@ -3061,30 +2336,8 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abcabc123", - matches: ["/abcabc123", "abc", "123"], - expected: { - path: "/abcabc123", - index: 0, - params: { foo: "abc", bar: "123" }, - }, - }, - { - input: "/abcabcabc123", - matches: ["/abcabcabc123", "abc", "abc123"], - expected: { - path: "/abcabcabc123", - index: 0, - params: { foo: "abc", bar: "abc123" }, - }, - }, - { - input: "/abcabcabc", - matches: ["/abcabcabc", "abc", "abc"], - expected: { - path: "/abcabcabc", - index: 0, - params: { foo: "abc", bar: "abc" }, - }, + matches: null, + expected: false, }, ], }, @@ -3094,12 +2347,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/abc", matches: ["/abc", "abc", undefined], - expected: { path: "/abc", index: 0, params: { foo: "abc" } }, + expected: { path: "/abc", params: { foo: "abc" } }, }, { input: "/abc.txt", matches: ["/abc.txt", "abc.txt", undefined], - expected: { path: "/abc.txt", index: 0, params: { foo: "abc.txt" } }, + expected: { path: "/abc.txt", params: { foo: "abc.txt" } }, }, ], }, @@ -3111,7 +2364,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/route|world|", "world"], expected: { path: "/route|world|", - index: 0, params: { param: "world" }, }, }, @@ -3130,7 +2382,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["/hello|world|", "hello", "world"], expected: { path: "/hello|world|", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -3147,7 +2398,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "x@y", matches: ["x@y", "x", "y"], - expected: { path: "x@y", index: 0, params: { foo: "x", bar: "y" } }, + expected: { path: "x@y", params: { foo: "x", bar: "y" } }, }, { input: "x@", @@ -3169,14 +2420,13 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "%25hello", matches: ["%25hello", "hello", undefined], - expected: { path: "%25hello", index: 0, params: { foo: "hello" } }, + expected: { path: "%25hello", params: { foo: "hello" } }, }, { input: "%25hello%25world", matches: ["%25hello%25world", "hello", "world"], expected: { path: "%25hello%25world", - index: 0, params: { foo: "hello", bar: "world" }, }, }, @@ -3185,7 +2435,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ matches: ["%25555%25222", "555", "222"], expected: { path: "%25555%25222", - index: 0, params: { foo: "555", bar: "222" }, }, }, diff --git a/src/index.spec.ts b/src/index.spec.ts index d0dd420..73f2c78 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { pathToRegexp, parse, compile, match } from "./index.js"; +import { parse, compile, match } from "./index.js"; import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; /** @@ -7,60 +7,48 @@ import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; */ describe("path-to-regexp", () => { describe("arguments", () => { - it("should accept an array of keys as the second argument", () => { - const re = pathToRegexp("/user/:id", { end: false }); - - const expectedKeys = [ - { - name: "id", - pattern: undefined, - }, - ]; - - expect(re.keys).toEqual(expectedKeys); - expect(exec(re, "/user/123/show")).toEqual(["/user/123", "123"]); - }); - - it("should accept parse result as input", () => { - const tokens = parse("/user/:id"); - const re = pathToRegexp(tokens); - expect(exec(re, "/user/123")).toEqual(["/user/123", "123"]); - }); - it("should throw on non-capturing pattern", () => { - expect(() => { - pathToRegexp("/:foo(?:\\d+(\\.\\d+)?)"); - }).toThrow(new TypeError('Pattern cannot start with "?" at 6')); + expect(() => match("/:foo(?:\\d+(\\.\\d+)?)")).toThrow( + new TypeError( + 'Pattern cannot start with "?" at 6: https://git.new/pathToRegexpError', + ), + ); }); it("should throw on nested capturing group", () => { - expect(() => { - pathToRegexp("/:foo(\\d+(\\.\\d+)?)"); - }).toThrow(new TypeError("Capturing groups are not allowed at 9")); + expect(() => match("/:foo(\\d+(\\.\\d+)?)")).toThrow( + new TypeError( + "Capturing groups are not allowed at 9: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on unbalanced pattern", () => { - expect(() => { - pathToRegexp("/:foo(abc"); - }).toThrow(new TypeError("Unbalanced pattern at 5")); + expect(() => match("/:foo(abc")).toThrow( + new TypeError( + "Unbalanced pattern at 5: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on missing pattern", () => { - expect(() => { - pathToRegexp("/:foo()"); - }).toThrow(new TypeError("Missing pattern at 5")); + expect(() => match("/:foo()")).toThrow( + new TypeError( + "Missing pattern at 5: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on missing name", () => { - expect(() => { - pathToRegexp("/:(test)"); - }).toThrow(new TypeError("Missing parameter name at 2")); + expect(() => match("/:(test)")).toThrow( + new TypeError( + "Missing parameter name at 2: https://git.new/pathToRegexpError", + ), + ); }); it("should throw on nested groups", () => { - expect(() => { - pathToRegexp("/{a{b:foo}}"); - }).toThrow( + expect(() => match("/{a{b:foo}}")).toThrow( new TypeError( "Unexpected { at 3, expected }: https://git.new/pathToRegexpError", ), @@ -68,14 +56,28 @@ describe("path-to-regexp", () => { }); it("should throw on repeat parameters without a separator", () => { - expect(() => { - pathToRegexp("{:x}*"); - }).toThrow( + expect(() => match("{:x}*")).toThrow( new TypeError( `Missing separator for "x": https://git.new/pathToRegexpError`, ), ); }); + + it("should throw on unterminated quote", () => { + expect(() => match('/:"foo')).toThrow( + new TypeError( + "Unterminated quote at 2: https://git.new/pathToRegexpError", + ), + ); + }); + + it("should throw on invalid *", () => { + expect(() => match("/:foo*")).toThrow( + new TypeError( + "Unexpected * at 5, you probably want `/*` or `{/:foo}*`: https://git.new/pathToRegexpError", + ), + ); + }); }); describe.each(PARSER_TESTS)( @@ -107,10 +109,9 @@ describe("path-to-regexp", () => { "match $path with $options", ({ path, options, tests }) => { it.each(tests)("should match $input", ({ input, matches, expected }) => { - const re = pathToRegexp(path, options); const fn = match(path, options); - expect(exec(re, input)).toEqual(matches); + expect(exec(fn.re, input)).toEqual(matches); expect(fn(input)).toEqual(expected); }); }, diff --git a/src/index.ts b/src/index.ts index 2dd3d6f..ec4fad1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,53 +25,25 @@ export interface ParseOptions { encodePath?: Encode; } -export interface PathToRegexpOptions extends ParseOptions { +export interface PathOptions { /** * Regexp will be case sensitive. (default: `false`) */ sensitive?: boolean; - /** - * Allow the delimiter to be arbitrarily repeated. (default: `true`) - */ - loose?: boolean; - /** - * Verify patterns are valid and safe to use. (default: `false`) - */ - strict?: boolean; - /** - * Match from the beginning of the string. (default: `true`) - */ - start?: boolean; - /** - * Match to the end of the string. (default: `true`) - */ - end?: boolean; - /** - * Allow optional trailing delimiter to match. (default: `true`) - */ - trailing?: boolean; } -export interface MatchOptions extends PathToRegexpOptions { +export interface MatchOptions extends PathOptions { /** * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) */ decode?: Decode | false; -} - -export interface CompileOptions extends ParseOptions { - /** - * Regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; - /** - * Allow the delimiter to be arbitrarily repeated. (default: `true`) - */ - loose?: boolean; /** - * Verify patterns are valid and safe to use. (default: `false`) + * Matches the path completely without trailing characters. (default: `true`) */ - strict?: boolean; + end?: boolean; +} + +export interface CompileOptions extends PathOptions { /** * Verifies the function is producing a valid path. (default: `true`) */ @@ -321,7 +293,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (asterisk) { tokens.push({ name: String(key++), - pattern: `(?:(?!${escape(delimiter)}).)*`, + pattern: `${negate(escape(delimiter))}*`, modifier: "*", separator: delimiter, }); @@ -362,33 +334,40 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { * Compile a string to a template function for the path. */ export function compile

( - path: Path, - options: CompileOptions = {}, + path: string, + options: CompileOptions & ParseOptions = {}, ) { - const data = path instanceof TokenData ? path : parse(path, options); - return compileTokens

(data, options); + return $compile

(parse(path, options), options); } export type ParamData = Partial>; export type PathFunction

= (data?: P) => string; +/** + * Check if a key repeats. + */ +export function isRepeat(key: Key) { + return key.modifier === "+" || key.modifier === "*"; +} + +/** + * Check if a key is optional. + */ +export function isOptional(key: Key) { + return key.modifier === "?" || key.modifier === "*"; +} + /** * Convert a single token into a path building function. */ -function tokenToFunction( - token: Token, +function keyToFunction( + token: Key, encode: Encode | false, ): (data: ParamData) => string { - if (typeof token === "string") { - return () => token; - } - const encodeValue = encode || NOOP_VALUE; - const repeated = token.modifier === "+" || token.modifier === "*"; - const optional = token.modifier === "?" || token.modifier === "*"; const { prefix = "", suffix = "", separator = suffix + prefix } = token; - if (encode && repeated) { + if (encode && isRepeat(token)) { const stringify = (value: string, index: number) => { if (typeof value !== "string") { throw new TypeError(`Expected "${token.name}/${index}" to be a string`); @@ -406,7 +385,7 @@ function tokenToFunction( return prefix + value.map(stringify).join(separator) + suffix; }; - if (optional) { + if (isOptional(token)) { return (data): string => { const value = data[token.name]; if (value == null) return ""; @@ -427,7 +406,7 @@ function tokenToFunction( return prefix + encodeValue(value) + suffix; }; - if (optional) { + if (isOptional(token)) { return (data): string => { const value = data[token.name]; if (value == null) return ""; @@ -444,25 +423,21 @@ function tokenToFunction( /** * Transform tokens into a path building function. */ -function compileTokens

( +export function $compile

( data: TokenData, options: CompileOptions, ): PathFunction

{ - const { - encode = encodeURIComponent, - loose = true, - validate = true, - strict = false, - } = options; + const { encode = encodeURIComponent, validate = true } = options; const flags = toFlags(options); - const stringify = toStringify(loose, data.delimiter); - const sources = toRegExpSource(data, stringify, [], flags, strict); + const sources = toRegExpSource(data, []); // Compile all the tokens into regexps. const encoders: Array<(data: ParamData) => string> = data.tokens.map( (token, index) => { - const fn = tokenToFunction(token, encode); - if (!validate || typeof token === "string") return fn; + if (typeof token === "string") return () => token; + + const fn = keyToFunction(token, encode); + if (!validate) return fn; const validRe = new RegExp(`^${sources[index]}$`, flags); @@ -490,7 +465,6 @@ function compileTokens

( */ export interface MatchResult

{ path: string; - index: number; params: P; } @@ -502,80 +476,83 @@ export type Match

= false | MatchResult

; /** * The match function takes a string and returns whether it matched the path. */ -export type MatchFunction

= (path: string) => Match

; +export type MatchFunction

= (( + path: string, +) => Match

) & { re: RegExp }; + +const isEnd = (input: string, match: string) => input.length === match.length; +const isDelimiterOrEnd = + (delimiter: string) => (input: string, match: string) => { + return ( + match.length === input.length || + input.slice(match.length, match.length + delimiter.length) === delimiter + ); + }; /** * Create path match function from `path-to-regexp` spec. */ -export function match

( - path: Path, +export function $match

( + data: TokenData, options: MatchOptions = {}, ): MatchFunction

{ - const { decode = decodeURIComponent, loose = true } = options; - const data = path instanceof TokenData ? path : parse(path, options); - const stringify = toStringify(loose, data.delimiter); - const keys: Key[] = []; - const re = tokensToRegexp(data, keys, options); + const { decode = decodeURIComponent, end = true } = options; + const re = tokensToRegexp(data, options); - const decoders = keys.map((key) => { + const decoders = re.keys.map((key) => { if (decode && (key.modifier === "+" || key.modifier === "*")) { const { prefix = "", suffix = "", separator = suffix + prefix } = key; - const re = new RegExp(stringify(separator), "g"); + const re = new RegExp(escape(separator), "g"); return (value: string) => value.split(re).map(decode); } return decode || NOOP_VALUE; }); - return function match(input: string) { - const m = re.exec(input); - if (!m) return false; + const validate = end ? isEnd : isDelimiterOrEnd(data.delimiter); - const { 0: path, index } = m; - const params = Object.create(null); + return Object.assign( + function match(input: string) { + const m = re.exec(input); + if (!m) return false; - for (let i = 1; i < m.length; i++) { - if (m[i] === undefined) continue; + const { 0: path } = m; + if (!validate(input, path)) return false; + const params = Object.create(null); - const key = keys[i - 1]; - const decoder = decoders[i - 1]; - params[key.name] = decoder(m[i]); - } + for (let i = 1; i < m.length; i++) { + if (m[i] === undefined) continue; - return { path, index, params }; - }; -} + const key = re.keys[i - 1]; + const decoder = decoders[i - 1]; + params[key.name] = decoder(m[i]); + } -/** - * Escape a regular expression string. - */ -function escape(str: string) { - return str.replace(/([.+*?^${}()[\]|/\\])/g, "\\$1"); + return { path, params }; + }, + { re }, + ); } -/** - * Escape and repeat loose characters for regular expressions. - */ -function looseReplacer(value: string, loose: string) { - const escaped = escape(value); - return loose ? `(?:${escaped})+(?!${escaped})` : escaped; +export function match

( + path: string, + options: MatchOptions & ParseOptions = {}, +): MatchFunction

{ + return $match(parse(path, options), options); } /** - * Encode all non-delimiter characters using the encode function. + * Escape a regular expression string. */ -function toStringify(loose: boolean, delimiter: string) { - if (!loose) return escape; - - const re = new RegExp(`(?:(?!${escape(delimiter)}).)+|(.)`, "g"); - return (value: string) => value.replace(re, looseReplacer); +function escape(str: string) { + return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); } /** * Get the flags for a regexp from the options. */ function toFlags(options: { sensitive?: boolean }) { - return options.sensitive ? "" : "i"; + return options.sensitive ? "s" : "is"; } /** @@ -598,47 +575,30 @@ export type Token = string | Key; /** * Expose a function for taking tokens and returning a RegExp. */ -function tokensToRegexp( - data: TokenData, - keys: Key[], - options: PathToRegexpOptions, -): RegExp { - const { - trailing = true, - loose = true, - start = true, - end = true, - strict = false, - } = options; +function tokensToRegexp(data: TokenData, options: PathOptions) { const flags = toFlags(options); - const stringify = toStringify(loose, data.delimiter); - const sources = toRegExpSource(data, stringify, keys, flags, strict); - let pattern = start ? "^" : ""; - pattern += sources.join(""); - if (trailing) pattern += `(?:${stringify(data.delimiter)})?`; - pattern += end ? "$" : `(?=${escape(data.delimiter)}|$)`; - - return new RegExp(pattern, flags); + const keys: Key[] = []; + const sources = toRegExpSource(data, keys); + const regexp = new RegExp(`^${sources.join("")}`, flags); + return Object.assign(regexp, { keys }); } /** * Convert a token into a regexp string (re-used for path validation). */ -function toRegExpSource( - data: TokenData, - stringify: Encode, - keys: Key[], - flags: string, - strict: boolean, -): string[] { - const defaultPattern = `(?:(?!${escape(data.delimiter)}).)+?`; +function toRegExpSource(data: TokenData, keys: Key[]): string[] { + const delim = escape(data.delimiter); + const sources = Array(data.tokens.length); let backtrack = ""; - let safe = true; - return data.tokens.map((token, index) => { + let i = data.tokens.length; + + while (i--) { + const token = data.tokens[i]; + if (typeof token === "string") { - backtrack = token; - return stringify(token); + sources[i] = backtrack = escape(token); + continue; } const { @@ -648,27 +608,17 @@ function toRegExpSource( modifier = "", } = token; - const pre = stringify(prefix); - const post = stringify(suffix); + const pre = escape(prefix); + const post = escape(suffix); if (token.name) { - const pattern = token.pattern ? `(?:${token.pattern})` : defaultPattern; - const re = checkPattern(pattern, token.name, flags); + let pattern = token.pattern || ""; - safe ||= safePattern(re, prefix || backtrack); - if (!safe) { - throw new TypeError( - `Ambiguous pattern for "${token.name}": ${DEBUG_URL}`, - ); - } - safe = !strict || safePattern(re, suffix); - backtrack = ""; + keys.unshift(token); - keys.push(token); - - if (modifier === "+" || modifier === "*") { + if (isRepeat(token)) { const mod = modifier === "*" ? "?" : ""; - const sep = stringify(separator); + const sep = escape(separator); if (!sep) { throw new TypeError( @@ -676,51 +626,25 @@ function toRegExpSource( ); } - safe ||= !strict || safePattern(re, separator); - if (!safe) { - throw new TypeError( - `Ambiguous pattern for "${token.name}" separator: ${DEBUG_URL}`, - ); - } - safe = !strict; - - return `(?:${pre}(${pattern}(?:${sep}${pattern})*)${post})${mod}`; + pattern ||= `${negate(delim, sep, post || backtrack)}+`; + sources[i] = + `(?:${pre}((?:${pattern})(?:${sep}(?:${pattern}))*)${post})${mod}`; + } else { + pattern ||= `${negate(delim, post || backtrack)}+`; + sources[i] = `(?:${pre}(${pattern})${post})${modifier}`; } - return `(?:${pre}(${pattern})${post})${modifier}`; + backtrack = pre || pattern; + } else { + sources[i] = `(?:${pre}${post})${modifier}`; + backtrack = `${pre}${post}`; } - - return `(?:${pre}${post})${modifier}`; - }); -} - -function checkPattern(pattern: string, name: string, flags: string) { - try { - return new RegExp(`^${pattern}$`, flags); - } catch (err: any) { - throw new TypeError(`Invalid pattern for "${name}": ${err.message}`); } -} -function safePattern(re: RegExp, value: string) { - return value ? !re.test(value) : false; + return sources; } -/** - * Repeated and simple input types. - */ -export type Path = string | TokenData; - -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - */ -export function pathToRegexp(path: Path, options: PathToRegexpOptions = {}) { - const data = path instanceof TokenData ? path : parse(path, options); - const keys: Key[] = []; - const regexp = tokensToRegexp(data, keys, options); - return Object.assign(regexp, { keys }); +function negate(...args: string[]) { + const values = Array.from(new Set(args)).filter(Boolean); + return `(?:(?!${values.join("|")}).)`; } From 6b7a4525b4d8633b6520987738ba04a732e1d6b2 Mon Sep 17 00:00:00 2001 From: Gregorio Litenstein Date: Wed, 24 Jul 2024 22:17:56 -0400 Subject: [PATCH 05/12] Throw on unmatched closing paren (#286) Co-authored-by: Blake Embrey --- src/index.spec.ts | 12 ++++++++++++ src/index.ts | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/index.spec.ts b/src/index.spec.ts index 73f2c78..b51dd9b 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -31,6 +31,18 @@ describe("path-to-regexp", () => { ); }); + it("should throw on unmatched )", function () { + expect(() => match("/:fooab)c")).toThrow( + new TypeError("Unmatched ) at 7: https://git.new/pathToRegexpError"), + ); + }); + + it("should throw on unmatched ) after other patterns", function () { + expect(() => match("/:test(\\w+)/:foo(\\d+))")).toThrow( + new TypeError("Unmatched ) at 21: https://git.new/pathToRegexpError"), + ); + }); + it("should throw on missing pattern", () => { expect(() => match("/:foo()")).toThrow( new TypeError( diff --git a/src/index.ts b/src/index.ts index ec4fad1..0d8bc53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,6 +199,10 @@ function lexer(str: string) { continue; } + if (value === ")") { + throw new TypeError(`Unmatched ) at ${i}: ${DEBUG_URL}`); + } + tokens.push({ type: "CHAR", index: i, value: chars[i++] }); } From 341f8737012c7be48d1d928ad34c7d6d1aa47984 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 19:28:32 -0700 Subject: [PATCH 06/12] Test for double decoding --- src/cases.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cases.spec.ts b/src/cases.spec.ts index a837ea4..0f1cbf3 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -299,6 +299,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ params: { test: ";,:@&=+$-_.!~*()" }, }, }, + { + input: "/param%2523", + matches: ["/param%2523", "param%2523"], + expected: { + path: "/param%2523", + params: { test: "param%23" }, + }, + }, ], }, From 0d20b150bb7ba7a3ecf1b993188a2584572326b9 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 24 Jul 2024 20:15:30 -0700 Subject: [PATCH 07/12] Split using a string --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0d8bc53..447228d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -506,8 +506,7 @@ export function $match

( const decoders = re.keys.map((key) => { if (decode && (key.modifier === "+" || key.modifier === "*")) { const { prefix = "", suffix = "", separator = suffix + prefix } = key; - const re = new RegExp(escape(separator), "g"); - return (value: string) => value.split(re).map(decode); + return (value: string) => value.split(separator).map(decode); } return decode || NOOP_VALUE; From 26551ecd97a63299e58e27f73ba3535195300034 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 31 Jul 2024 15:17:18 -0700 Subject: [PATCH 08/12] Code gen smaller and faster regexps --- src/index.ts | 62 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index 447228d..9826439 100644 --- a/src/index.ts +++ b/src/index.ts @@ -297,7 +297,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { if (asterisk) { tokens.push({ name: String(key++), - pattern: `${negate(escape(delimiter))}*`, + pattern: `${negate(delimiter)}*`, modifier: "*", separator: delimiter, }); @@ -590,7 +590,6 @@ function tokensToRegexp(data: TokenData, options: PathOptions) { * Convert a token into a regexp string (re-used for path validation). */ function toRegExpSource(data: TokenData, keys: Key[]): string[] { - const delim = escape(data.delimiter); const sources = Array(data.tokens.length); let backtrack = ""; @@ -600,7 +599,8 @@ function toRegExpSource(data: TokenData, keys: Key[]): string[] { const token = data.tokens[i]; if (typeof token === "string") { - sources[i] = backtrack = escape(token); + backtrack = token; + sources[i] = escape(token); continue; } @@ -615,32 +615,40 @@ function toRegExpSource(data: TokenData, keys: Key[]): string[] { const post = escape(suffix); if (token.name) { - let pattern = token.pattern || ""; - + backtrack = suffix || backtrack; keys.unshift(token); if (isRepeat(token)) { - const mod = modifier === "*" ? "?" : ""; - const sep = escape(separator); - - if (!sep) { + if (!separator) { throw new TypeError( `Missing separator for "${token.name}": ${DEBUG_URL}`, ); } - pattern ||= `${negate(delim, sep, post || backtrack)}+`; - sources[i] = - `(?:${pre}((?:${pattern})(?:${sep}(?:${pattern}))*)${post})${mod}`; + const mod = modifier === "*" ? "?" : ""; + const sep = escape(separator); + const pattern = + token.pattern || `${negate(data.delimiter, separator, backtrack)}+`; + + sources[i] = wrap( + pre, + `(?:${pattern})(?:${sep}(?:${pattern}))*`, + post, + mod, + ); } else { - pattern ||= `${negate(delim, post || backtrack)}+`; - sources[i] = `(?:${pre}(${pattern})${post})${modifier}`; + sources[i] = wrap( + pre, + token.pattern || `${negate(data.delimiter, backtrack)}+`, + post, + modifier, + ); } - backtrack = pre || pattern; + backtrack = prefix; } else { sources[i] = `(?:${pre}${post})${modifier}`; - backtrack = `${pre}${post}`; + backtrack = `${prefix}${suffix}`; } } @@ -648,6 +656,24 @@ function toRegExpSource(data: TokenData, keys: Key[]): string[] { } function negate(...args: string[]) { - const values = Array.from(new Set(args)).filter(Boolean); - return `(?:(?!${values.join("|")}).)`; + const values = args.sort().filter((value, index, array) => { + for (let i = 0; i < index; i++) { + const v = array[i]; + if (v.length && value.startsWith(v)) return false; + } + return value.length > 0; + }); + + const isSimple = values.every((value) => value.length === 1); + if (isSimple) return `[^${escape(values.join(""))}]`; + + return `(?:(?!${values.map(escape).join("|")}).)`; +} + +function wrap(pre: string, pattern: string, post: string, modifier: string) { + if (pre || post) { + return `(?:${pre}(${pattern})${post})${modifier}`; + } + + return `(${pattern})${modifier}`; } From fb4d11d38f81e336a73a2ca0887e8105dd6d72ca Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Wed, 31 Jul 2024 15:20:52 -0700 Subject: [PATCH 09/12] Remove matches from tests --- scripts/redos.ts | 2 +- src/cases.spec.ts | 349 +++++++++------------------------------------- src/index.spec.ts | 4 +- src/index.ts | 73 ++++------ 4 files changed, 98 insertions(+), 330 deletions(-) diff --git a/scripts/redos.ts b/scripts/redos.ts index fe2c7ac..f83e52d 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -14,7 +14,7 @@ const TESTS = new Set(MATCH_TESTS.map((test) => test.path)); // ]; for (const path of TESTS) { - const { re } = match(path); + const { re } = match(path) as any; const result = checkSync(re.source, re.flags); if (result.status === "safe") { safe++; diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 0f1cbf3..508b946 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -27,7 +27,6 @@ export interface MatchTestSet { options?: MatchOptions & ParseOptions; tests: Array<{ input: string; - matches: (string | undefined)[] | null; expected: Match; }>; } @@ -212,10 +211,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/"], expected: { path: "/", params: {} }, }, - { input: "/route", matches: ["/"], expected: false }, + { input: "/route", expected: false }, ], }, { @@ -223,14 +221,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test"], expected: { path: "/test", params: {} }, }, - { input: "/route", matches: null, expected: false }, - { input: "/test/route", matches: ["/test"], expected: false }, + { input: "/route", expected: false }, + { input: "/test/route", expected: false }, { input: "/test/", - matches: ["/test"], expected: false, }, ], @@ -240,14 +236,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test/", - matches: ["/test/"], expected: { path: "/test/", params: {} }, }, - { input: "/route", matches: null, expected: false }, - { input: "/test", matches: null, expected: false }, + { input: "/route", expected: false }, + { input: "/test", expected: false }, { input: "/test//", - matches: ["/test/"], expected: false, }, ], @@ -257,17 +251,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route", "route"], expected: false, }, { input: "/route.json", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -275,17 +266,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json/", - matches: ["/route.json", "route.json"], expected: false, }, { input: "/route/test", - matches: ["/route", "route"], expected: false, }, { input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", params: { test: "café" }, @@ -293,7 +281,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/;,:@&=+$-_.!~*()", - matches: ["/;,:@&=+$-_.!~*()", ";,:@&=+$-_.!~*()"], expected: { path: "/;,:@&=+$-_.!~*()", params: { test: ";,:@&=+$-_.!~*()" }, @@ -301,7 +288,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/param%2523", - matches: ["/param%2523", "param%2523"], expected: { path: "/param%2523", params: { test: "param%23" }, @@ -321,10 +307,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test"], expected: { path: "/test", params: {} }, }, - { input: "/TEST", matches: null, expected: false }, + { input: "/TEST", expected: false }, ], }, { @@ -335,10 +320,9 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/TEST", - matches: ["/TEST"], expected: { path: "/TEST", params: {} }, }, - { input: "/test", matches: null, expected: false }, + { input: "/test", expected: false }, ], }, @@ -353,32 +337,26 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/test/", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/test////", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/route/test", - matches: null, expected: false, }, { input: "/test/route", - matches: ["/test"], expected: { path: "/test", params: {} }, }, { input: "/route", - matches: null, expected: false, }, ], @@ -391,27 +369,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test/", - matches: ["/test/"], expected: { path: "/test/", params: {} }, }, { input: "/test//", - matches: ["/test/"], expected: { path: "/test/", params: {} }, }, { input: "/test/route", - matches: ["/test/"], expected: false, }, { input: "/route/test/deep", - matches: null, expected: false, }, ], @@ -424,17 +397,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -442,7 +412,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json/", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -450,12 +419,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route/test", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json/test", - matches: ["/route.json", "route.json"], expected: { path: "/route.json", params: { test: "route.json" }, @@ -463,7 +430,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", params: { test: "café" }, @@ -479,27 +445,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: null, expected: false, }, { input: "/route/", - matches: ["/route/", "route"], expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route/test", - matches: ["/route/", "route"], expected: false, }, { input: "/route/test/", - matches: ["/route/", "route"], expected: false, }, { input: "/route//test", - matches: ["/route/", "route"], expected: { path: "/route/", params: { test: "route" } }, }, ], @@ -512,27 +473,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "", - matches: [""], expected: { path: "", params: {} }, }, { input: "/", - matches: [""], expected: { path: "", params: {} }, }, { input: "route", - matches: [""], expected: false, }, { input: "/route", - matches: [""], expected: { path: "", params: {} }, }, { input: "/route/", - matches: [""], expected: { path: "", params: {} }, }, ], @@ -546,17 +502,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "", - matches: ["", undefined], expected: { path: "", params: {} }, }, { input: "/", - matches: ["", undefined], expected: false, }, ], @@ -566,17 +519,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/bar", - matches: ["/bar", undefined], expected: { path: "/bar", params: {} }, }, { input: "/foo/bar", - matches: ["/foo/bar", "foo"], expected: { path: "/foo/bar", params: { test: "foo" } }, }, { input: "/foo/bar/", - matches: ["/foo/bar", "foo"], expected: false, }, ], @@ -586,17 +536,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: ["-bar", undefined], expected: { path: "-bar", params: {} }, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar", "foo"], expected: false, }, ], @@ -606,17 +553,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/-bar", - matches: ["/-bar", undefined], expected: { path: "/-bar", params: {} }, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: "foo" } }, }, { input: "/foo-bar/", - matches: ["/foo-bar", "foo"], expected: false, }, ], @@ -630,22 +574,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["", undefined], expected: false, }, { input: "//", - matches: ["", undefined], expected: false, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", - matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", params: { test: ["some", "basic", "route"] }, @@ -658,22 +598,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: ["-bar", undefined], expected: { path: "-bar", params: {} }, }, { input: "/-bar", - matches: null, expected: false, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", - matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", params: { test: ["foo", "baz"] }, @@ -690,22 +626,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: null, expected: false, }, { input: "//", - matches: null, expected: false, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: ["route"] } }, }, { input: "/some/basic/route", - matches: ["/some/basic/route", "some/basic/route"], expected: { path: "/some/basic/route", params: { test: ["some", "basic", "route"] }, @@ -718,22 +650,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: null, expected: false, }, { input: "/-bar", - matches: null, expected: false, }, { input: "/foo-bar", - matches: ["/foo-bar", "foo"], expected: { path: "/foo-bar", params: { test: ["foo"] } }, }, { input: "/foo/baz-bar", - matches: ["/foo/baz-bar", "foo/baz"], expected: { path: "/foo/baz-bar", params: { test: ["foo", "baz"] }, @@ -750,17 +678,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", - matches: null, expected: false, }, { input: "/123/abc", - matches: ["/123", "123"], expected: false, }, ], @@ -770,27 +695,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "-bar", - matches: null, expected: false, }, { input: "/-bar", - matches: null, expected: false, }, { input: "/abc-bar", - matches: null, expected: false, }, { input: "/123-bar", - matches: ["/123-bar", "123"], expected: { path: "/123-bar", params: { test: "123" } }, }, { input: "/123/456-bar", - matches: null, expected: false, }, ], @@ -800,17 +720,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", ""], expected: { path: "/", params: { test: "" } }, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route/123", - matches: ["/route/123", "route/123"], expected: { path: "/route/123", params: { test: "route/123" }, @@ -818,7 +735,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/;,:@&=/+$-_.!/~*()", - matches: ["/;,:@&=/+$-_.!/~*()", ";,:@&=/+$-_.!/~*()"], expected: { path: "/;,:@&=/+$-_.!/~*()", params: { test: ";,:@&=/+$-_.!/~*()" }, @@ -831,17 +747,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", "abc"], expected: { path: "/abc", params: { test: "abc" } }, }, { input: "/123", - matches: null, expected: false, }, { input: "/abc/123", - matches: ["/abc", "abc"], expected: false, }, ], @@ -851,17 +764,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/this", - matches: ["/this", "this"], expected: { path: "/this", params: { test: "this" } }, }, { input: "/that", - matches: ["/that", "that"], expected: { path: "/that", params: { test: "that" } }, }, { input: "/foo", - matches: null, expected: false, }, ], @@ -871,17 +781,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["", undefined], expected: false, }, { input: "/abc", - matches: ["/abc", "abc"], expected: { path: "/abc", params: { test: ["abc"] } }, }, { input: "/abc/abc", - matches: ["/abc/abc", "abc/abc"], expected: { path: "/abc/abc", params: { test: ["abc", "abc"] }, @@ -889,7 +796,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/xyz/xyz", - matches: ["/xyz/xyz", "xyz/xyz"], expected: { path: "/xyz/xyz", params: { test: ["xyz", "xyz"] }, @@ -897,7 +803,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abc/xyz", - matches: ["/abc/xyz", "abc/xyz"], expected: { path: "/abc/xyz", params: { test: ["abc", "xyz"] }, @@ -905,7 +810,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abc/xyz/abc/xyz", - matches: ["/abc/xyz/abc/xyz", "abc/xyz/abc/xyz"], expected: { path: "/abc/xyz/abc/xyz", params: { test: ["abc", "xyz", "abc", "xyz"] }, @@ -913,7 +817,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/xyzxyz", - matches: ["/xyz", "xyz"], expected: false, }, ], @@ -927,12 +830,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "test", - matches: ["test"], expected: { path: "test", params: {} }, }, { input: "/test", - matches: null, expected: false, }, ], @@ -942,17 +843,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "route", - matches: ["route", "route"], expected: { path: "route", params: { test: "route" } }, }, { input: "/route", - matches: null, expected: false, }, { input: "route/", - matches: ["route", "route"], expected: false, }, ], @@ -962,12 +860,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "test", - matches: ["test", "test"], expected: { path: "test", params: { test: "test" } }, }, { input: "", - matches: ["", undefined], expected: { path: "", params: {} }, }, ], @@ -977,22 +873,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "route/", - matches: ["route/", "route"], expected: { path: "route/", params: { test: ["route"] } }, }, { input: "/route", - matches: null, expected: false, }, { input: "", - matches: null, expected: false, }, { input: "foo/bar/", - matches: ["foo/bar/", "foo/bar"], expected: { path: "foo/bar/", params: { test: ["foo", "bar"] }, @@ -1009,12 +901,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test.json", - matches: ["/test.json"], expected: { path: "/test.json", params: {} }, }, { input: "/test", - matches: null, expected: false, }, ], @@ -1024,22 +914,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/.json", - matches: null, expected: false, }, { input: "/test.json", - matches: ["/test.json", "test"], expected: { path: "/test.json", params: { test: "test" } }, }, { input: "/route.json", - matches: ["/route.json", "route"], expected: { path: "/route.json", params: { test: "route" } }, }, { input: "/route.json.json", - matches: ["/route.json", "route"], expected: false, }, ], @@ -1049,7 +935,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route.json.json", - matches: ["/route.json.json", "route.json"], expected: { path: "/route.json.json", params: { test: "route.json" }, @@ -1066,12 +951,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: "html" } }, }, { input: "/test", - matches: null, expected: false, }, ], @@ -1081,7 +964,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test.html.json", - matches: ["/test.html.json", "html", "json"], expected: { path: "/test.html.json", params: { format: "json" }, @@ -1089,7 +971,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test.html", - matches: null, expected: false, }, ], @@ -1099,12 +980,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: ["/test", undefined], expected: { path: "/test", params: { format: undefined } }, }, { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: "html" } }, }, ], @@ -1114,12 +993,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: ["html"] }, @@ -1127,7 +1004,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test.html.json", - matches: ["/test.html.json", "html.json"], expected: { path: "/test.html.json", params: { format: ["html", "json"] }, @@ -1140,12 +1016,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test.html", - matches: ["/test.html", "html"], expected: { path: "/test.html", params: { format: ["html"] }, @@ -1153,7 +1027,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test.hbs.html", - matches: ["/test.hbs.html", "hbs.html"], expected: { path: "/test.hbs.html", params: { format: ["hbs", "html"] }, @@ -1170,7 +1043,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route.html", - matches: ["/route.html", "route", "html"], expected: { path: "/route.html", params: { test: "route", format: "html" }, @@ -1178,12 +1050,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route", - matches: null, expected: false, }, { input: "/route.html.json", - matches: ["/route.html.json", "route", "html.json"], expected: { path: "/route.html.json", params: { test: "route", format: "html.json" }, @@ -1196,12 +1066,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route", undefined], expected: { path: "/route", params: { test: "route" } }, }, { input: "/route.json", - matches: ["/route.json", "route", "json"], expected: { path: "/route.json", params: { test: "route", format: "json" }, @@ -1209,7 +1077,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json.html", - matches: ["/route.json.html", "route", "json.html"], expected: { path: "/route.json.html", params: { test: "route", format: "json.html" }, @@ -1222,7 +1089,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route.htmlz", - matches: ["/route.htmlz", "route", "html"], expected: { path: "/route.htmlz", params: { test: "route", format: "html" }, @@ -1230,7 +1096,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.html", - matches: null, expected: false, }, ], @@ -1244,17 +1109,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { "0": "123" } }, }, { input: "/abc", - matches: null, expected: false, }, { input: "/123/abc", - matches: ["/123", "123"], expected: false, }, ], @@ -1264,12 +1126,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["", undefined], expected: false, }, { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { "0": "123" } }, }, ], @@ -1279,7 +1139,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route(\\123\\)", - matches: ["/route(\\123\\)", "123\\"], expected: { path: "/route(\\123\\)", params: { "0": "123\\" }, @@ -1287,7 +1146,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route(\\123)", - matches: null, expected: false, }, ], @@ -1297,22 +1155,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "", - matches: [""], expected: { path: "", params: {} }, }, { input: "/", - matches: [""], expected: false, }, { input: "/foo", - matches: [""], expected: false, }, { input: "/route", - matches: ["/route"], expected: { path: "/route", params: {} }, }, ], @@ -1322,12 +1176,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", ""], expected: { path: "/", params: { "0": "" } }, }, { input: "/login", - matches: ["/login", "login"], expected: { path: "/login", params: { "0": "login" } }, }, ], @@ -1341,12 +1193,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/testing", - matches: null, expected: false, }, { input: "/(testing)", - matches: ["/(testing)"], expected: { path: "/(testing)", params: {} }, }, ], @@ -1356,7 +1206,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/.+*?{}=^!:$[]|", - matches: ["/.+*?{}=^!:$[]|"], expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, ], @@ -1366,12 +1215,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test/u123", - matches: ["/test/u123", "u123", undefined], expected: { path: "/test/u123", params: { uid: "u123" } }, }, { input: "/test/c123", - matches: ["/test/c123", undefined, "c123"], expected: { path: "/test/c123", params: { cid: "c123" } }, }, ], @@ -1385,12 +1232,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/icon-240.png", - matches: ["/icon-240.png", "240"], expected: { path: "/icon-240.png", params: { res: "240" } }, }, { input: "/apple-icon-240.png", - matches: ["/apple-icon-240.png", "240"], expected: { path: "/apple-icon-240.png", params: { res: "240" }, @@ -1407,7 +1252,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/match/route", - matches: ["/match/route", "match", "route"], expected: { path: "/match/route", params: { foo: "match", bar: "route" }, @@ -1420,12 +1264,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foo(test)/bar", - matches: ["/foo(test)/bar", "foo"], expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, }, { input: "/foo/bar", - matches: null, expected: false, }, ], @@ -1435,7 +1277,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/endpoint/user", - matches: ["/endpoint/user", "endpoint", "user"], expected: { path: "/endpoint/user", params: { remote: "endpoint", user: "user" }, @@ -1443,7 +1284,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/endpoint/user-name", - matches: ["/endpoint/user-name", "endpoint", "user-name"], expected: { path: "/endpoint/user-name", params: { remote: "endpoint", user: "user-name" }, @@ -1451,7 +1291,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo.bar/user-name", - matches: ["/foo.bar/user-name", "foo.bar", "user-name"], expected: { path: "/foo.bar/user-name", params: { remote: "foo.bar", user: "user-name" }, @@ -1464,12 +1303,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route?", - matches: ["/route?", "route"], expected: { path: "/route?", params: { foo: "route" } }, }, { input: "/route", - matches: null, expected: false, }, ], @@ -1479,17 +1316,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foobar", - matches: ["/foobar", "foo"], expected: { path: "/foobar", params: { foo: ["foo"] } }, }, { input: "/foo/bar", - matches: null, expected: false, }, { input: "/foo/barbar", - matches: null, expected: false, }, ], @@ -1499,12 +1333,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foobaz", - matches: ["/foobaz", "foo"], expected: { path: "/foobaz", params: { pre: "foo" } }, }, { input: "/baz", - matches: ["/baz", undefined], expected: { path: "/baz", params: { pre: undefined } }, }, ], @@ -1514,7 +1346,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/hello(world)", - matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", params: { foo: "hello", bar: "world" }, @@ -1522,7 +1353,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/hello()", - matches: null, expected: false, }, ], @@ -1532,7 +1362,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/hello(world)", - matches: ["/hello(world)", "hello", "world"], expected: { path: "/hello(world)", params: { foo: "hello", bar: "world" }, @@ -1540,7 +1369,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/hello()", - matches: ["/hello()", "hello", undefined], expected: { path: "/hello()", params: { foo: "hello", bar: undefined }, @@ -1553,12 +1381,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/video", - matches: ["/video", "video", undefined], expected: { path: "/video", params: { postType: "video" } }, }, { input: "/video+test", - matches: ["/video+test", "video", "+test"], expected: { path: "/video+test", params: { 0: "+test", postType: "video" }, @@ -1566,7 +1392,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/video+", - matches: ["/video", "video", undefined], expected: false, }, ], @@ -1576,12 +1401,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/-ext", - matches: null, expected: false, }, { input: "-ext", - matches: ["-ext", undefined, undefined], expected: { path: "-ext", params: { foo: undefined, bar: undefined }, @@ -1589,12 +1412,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo-ext", - matches: ["/foo-ext", "foo", undefined], expected: { path: "/foo-ext", params: { foo: "foo" } }, }, { input: "/foo/bar-ext", - matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", params: { foo: "foo", bar: "bar" }, @@ -1602,7 +1423,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/-ext", - matches: null, expected: false, }, ], @@ -1612,12 +1432,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foo-ext", - matches: ["/foo-ext", "foo", undefined], expected: { path: "/foo-ext", params: { required: "foo" } }, }, { input: "/foo/bar-ext", - matches: ["/foo/bar-ext", "foo", "bar"], expected: { path: "/foo/bar-ext", params: { required: "foo", optional: "bar" }, @@ -1625,7 +1443,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/-ext", - matches: null, expected: false, }, ], @@ -1639,7 +1456,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/café", - matches: ["/café", "café"], expected: { path: "/café", params: { foo: "café" } }, }, ], @@ -1652,7 +1468,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/caf%C3%A9", - matches: ["/caf%C3%A9", "caf%C3%A9"], expected: { path: "/caf%C3%A9", params: { foo: "caf%C3%A9" }, @@ -1665,7 +1480,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/café", - matches: ["/café"], expected: { path: "/café", params: {} }, }, ], @@ -1678,7 +1492,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/caf%C3%A9", - matches: ["/caf%C3%A9"], expected: { path: "/caf%C3%A9", params: {} }, }, ], @@ -1695,7 +1508,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "example.com", - matches: ["example.com", "example"], expected: { path: "example.com", params: { domain: "example" }, @@ -1703,7 +1515,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "github.com", - matches: ["github.com", "github"], expected: { path: "github.com", params: { domain: "github" }, @@ -1719,7 +1530,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "mail.example.com", - matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", params: { domain: "example" }, @@ -1727,7 +1537,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "mail.github.com", - matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", params: { domain: "github" }, @@ -1743,12 +1552,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "mail.com", - matches: ["mail.com", undefined], expected: { path: "mail.com", params: { domain: undefined } }, }, { input: "mail.example.com", - matches: ["mail.example.com", "example"], expected: { path: "mail.example.com", params: { domain: "example" }, @@ -1756,7 +1563,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "mail.github.com", - matches: ["mail.github.com", "github"], expected: { path: "mail.github.com", params: { domain: "github" }, @@ -1772,12 +1578,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "example.com", - matches: ["example.com", "com"], expected: { path: "example.com", params: { ext: "com" } }, }, { input: "example.org", - matches: ["example.org", "org"], expected: { path: "example.org", params: { ext: "org" } }, }, ], @@ -1791,12 +1595,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "this is a test", - matches: ["this is"], expected: { path: "this is", params: {} }, }, { input: "this isn't", - matches: ["this is"], expected: false, }, ], @@ -1810,12 +1612,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "$x", - matches: ["$x", "x", undefined], expected: { path: "$x", params: { foo: "x" } }, }, { input: "$x$y", - matches: ["$x$y", "x", "y"], expected: { path: "$x$y", params: { foo: "x", bar: "y" } }, }, ], @@ -1825,12 +1625,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "$x", - matches: ["$x", "x"], expected: { path: "$x", params: { foo: ["x"] } }, }, { input: "$x$y", - matches: ["$x$y", "x$y"], expected: { path: "$x$y", params: { foo: ["x", "y"] } }, }, ], @@ -1840,12 +1638,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "name", - matches: ["name", undefined, undefined, undefined], expected: { path: "name", params: {} }, }, { input: "name/test", - matches: ["name/test", "test", undefined, undefined], expected: { path: "name/test", params: { attr1: "test" }, @@ -1853,7 +1649,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1", - matches: ["name/1", "1", undefined, undefined], expected: { path: "name/1", params: { attr1: "1" }, @@ -1861,7 +1656,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2", - matches: ["name/1-2", "1", "2", undefined], expected: { path: "name/1-2", params: { attr1: "1", attr2: "2" }, @@ -1869,7 +1663,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2-3", - matches: ["name/1-2-3", "1", "2", "3"], expected: { path: "name/1-2-3", params: { attr1: "1", attr2: "2", attr3: "3" }, @@ -1877,12 +1670,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/foo-bar/route", - matches: ["name/foo-bar", "foo", "bar", undefined], expected: false, }, { input: "name/test/route", - matches: ["name/test", "test", undefined, undefined], expected: false, }, ], @@ -1892,12 +1683,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "name", - matches: ["name", undefined], expected: { path: "name", params: {} }, }, { input: "name/1", - matches: ["name/1", "1"], expected: { path: "name/1", params: { attrs: ["1"] }, @@ -1905,7 +1694,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2", - matches: ["name/1-2", "1-2"], expected: { path: "name/1-2", params: { attrs: ["1", "2"] }, @@ -1913,7 +1701,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/1-2-3", - matches: ["name/1-2-3", "1-2-3"], expected: { path: "name/1-2-3", params: { attrs: ["1", "2", "3"] }, @@ -1921,12 +1708,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "name/foo-bar/route", - matches: ["name/foo-bar", "foo-bar"], expected: false, }, { input: "name/test/route", - matches: ["name/test", "test"], expected: false, }, ], @@ -1940,27 +1725,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/123", - matches: ["/123", "123"], expected: { path: "/123", params: { test: "123" } }, }, { input: "/abc", - matches: null, expected: false, }, { input: "/123/abc", - matches: ["/123", "123"], expected: false, }, { input: "/123.123", - matches: ["/123.123", "123.123"], expected: { path: "/123.123", params: { test: "123.123" } }, }, { input: "/123.abc", - matches: ["/123", "123"], expected: false, }, ], @@ -1970,12 +1750,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { test: "route" } }, }, { input: "/login", - matches: null, expected: false, }, ], @@ -1989,12 +1767,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/user/123", - matches: ["/user/123", undefined, "123"], expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", - matches: ["/users/123", "s", "123"], expected: { path: "/users/123", params: { 0: "s", user: "123" }, @@ -2007,12 +1783,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/user/123", - matches: ["/user/123", "123"], expected: { path: "/user/123", params: { user: "123" } }, }, { input: "/users/123", - matches: ["/users/123", "123"], expected: { path: "/users/123", params: { user: "123" } }, }, ], @@ -2026,7 +1800,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/files/hello/world.txt", - matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", params: { path: ["hello", "world"], ext: ["txt"] }, @@ -2034,7 +1807,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/files/hello/world.txt.png", - matches: ["/files/hello/world.txt.png", "hello/world", "txt.png"], expected: { path: "/files/hello/world.txt.png", params: { path: ["hello", "world"], ext: ["txt", "png"] }, @@ -2042,7 +1814,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg", "my/photo", "jpg"], expected: false, }, ], @@ -2052,7 +1823,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/files/hello/world.txt", - matches: ["/files/hello/world.txt", "hello/world", "txt"], expected: { path: "/files/hello/world.txt", params: { path: ["hello", "world"], ext: "txt" }, @@ -2060,7 +1830,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/files/my/photo.jpg/gif", - matches: ["/files/my/photo.jpg", "my/photo", "jpg"], expected: false, }, ], @@ -2070,7 +1839,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "#/", - matches: ["#/", undefined], expected: { path: "#/", params: {} }, }, ], @@ -2080,7 +1848,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/foo/test1/test2", - matches: ["/foo/test1/test2", "test1/test2"], expected: { path: "/foo/test1/test2", params: { bar: ["test1", "test2"] }, @@ -2093,12 +1860,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/entity/foo", - matches: null, expected: false, }, { input: "/entity/foo/", - matches: ["/entity/foo/", "foo", undefined], expected: { path: "/entity/foo/", params: { id: "foo" } }, }, ], @@ -2108,22 +1873,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/test", - matches: null, expected: false, }, { input: "/test/", - matches: ["/test/", undefined], expected: { path: "/test/", params: {} }, }, { input: "/test/route", - matches: ["/test/route", "route"], expected: { path: "/test/route", params: { "0": ["route"] } }, }, { input: "/test/route/nested", - matches: ["/test/route/nested", "route/nested"], expected: { path: "/test/route/nested", params: { "0": ["route", "nested"] }, @@ -2140,17 +1901,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", undefined], expected: { path: "/", params: { "0": undefined } }, }, { input: "/route", - matches: ["/route", "route"], expected: { path: "/route", params: { "0": ["route"] } }, }, { input: "/route/nested", - matches: ["/route/nested", "route/nested"], expected: { path: "/route/nested", params: { "0": ["route", "nested"] }, @@ -2163,12 +1921,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", "/"], expected: { path: "/", params: { "0": ["", ""] } }, }, { input: "/test", - matches: ["/test", "/test"], expected: { path: "/test", params: { "0": ["", "test"] } }, }, ], @@ -2179,16 +1935,72 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", "/"], expected: { path: "/", params: { "0": "/" } }, }, { input: "/test", - matches: ["/test", "/test"], expected: { path: "/test", params: { "0": "/test" } }, }, ], }, + { + path: "/*.:ext", + tests: [ + { + input: "/test.html", + expected: { + path: "/test.html", + params: { "0": ["test"], ext: "html" }, + }, + }, + { + input: "/test.html/nested", + expected: false, + }, + ], + }, + { + path: "/*{.:ext}?", + tests: [ + { + input: "/test.html", + expected: { + path: "/test.html", + params: { "0": ["test.html"], ext: undefined }, + }, + }, + { + input: "/test.html/nested", + expected: { + params: { + "0": ["test.html", "nested"], + }, + path: "/test.html/nested", + }, + }, + ], + }, + { + path: "/*{.:ext}*", + tests: [ + { + input: "/test.html", + expected: { + path: "/test.html", + params: { "0": ["test.html"], ext: undefined }, + }, + }, + { + input: "/test.html/nested", + expected: { + params: { + "0": ["test.html", "nested"], + }, + path: "/test.html/nested", + }, + }, + ], + }, /** * Longer prefix. @@ -2198,12 +2010,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route", - matches: ["/route", "route", undefined], expected: { path: "/route", params: { foo: "route" } }, }, { input: "/route/test/again", - matches: ["/route/test/again", "route", "again"], expected: { path: "/route/test/again", params: { foo: "route", bar: "again" }, @@ -2220,12 +2030,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/", - matches: ["/", "test"], expected: { path: "/", params: { foo: ["test"] } }, }, { input: "/", - matches: ["/", "test>", params: { foo: ["test", "again"] }, @@ -2242,12 +2050,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "", - matches: ["", undefined, undefined], expected: { path: "", params: {} }, }, { input: "test/", - matches: ["test/", "test", undefined], expected: { path: "test/", params: { foo: "test" }, @@ -2255,7 +2061,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "a/b.", - matches: ["a/b.", "a", "b"], expected: { path: "a/b.", params: { foo: "a", bar: "b" } }, }, ], @@ -2265,22 +2070,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", undefined], expected: { path: "/abc", params: {} }, }, { input: "/abcabc", - matches: ["/abc", undefined], expected: false, }, { input: "/abcabc123", - matches: ["/abcabc123", "123"], expected: { path: "/abcabc123", params: { foo: "123" } }, }, { input: "/abcabcabc123", - matches: ["/abcabcabc123", "abc123"], expected: { path: "/abcabcabc123", params: { foo: "abc123" }, @@ -2288,7 +2089,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abcabcabc", - matches: ["/abcabcabc", "abc"], expected: { path: "/abcabcabc", params: { foo: "abc" } }, }, ], @@ -2298,22 +2098,18 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: null, expected: false, }, { input: "/abcabc", - matches: null, expected: false, }, { input: "/abcabc123", - matches: null, expected: false, }, { input: "/acb", - matches: ["/acb", "acb", undefined], expected: { path: "/acb", params: { foo: "acb" }, @@ -2321,7 +2117,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/acbabc123", - matches: ["/acbabc123", "acb", "123"], expected: { path: "/acbabc123", params: { foo: "acb", bar: "123" }, @@ -2334,17 +2129,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: null, expected: false, }, { input: "/abcabc", - matches: null, expected: false, }, { input: "/abcabc123", - matches: null, expected: false, }, ], @@ -2354,12 +2146,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/abc", - matches: ["/abc", "abc", undefined], expected: { path: "/abc", params: { foo: "abc" } }, }, { input: "/abc.txt", - matches: ["/abc.txt", "abc.txt", undefined], expected: { path: "/abc.txt", params: { foo: "abc.txt" } }, }, ], @@ -2369,7 +2159,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/route|world|", - matches: ["/route|world|", "world"], expected: { path: "/route|world|", params: { param: "world" }, @@ -2377,7 +2166,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route||", - matches: null, expected: false, }, ], @@ -2387,7 +2175,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "/hello|world|", - matches: ["/hello|world|", "hello", "world"], expected: { path: "/hello|world|", params: { foo: "hello", bar: "world" }, @@ -2395,7 +2182,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/hello||", - matches: null, expected: false, }, ], @@ -2405,12 +2191,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "x@y", - matches: ["x@y", "x", "y"], expected: { path: "x@y", params: { foo: "x", bar: "y" } }, }, { input: "x@", - matches: null, expected: false, }, ], @@ -2427,12 +2211,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ tests: [ { input: "%25hello", - matches: ["%25hello", "hello", undefined], expected: { path: "%25hello", params: { foo: "hello" } }, }, { input: "%25hello%25world", - matches: ["%25hello%25world", "hello", "world"], expected: { path: "%25hello%25world", params: { foo: "hello", bar: "world" }, @@ -2440,7 +2222,6 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "%25555%25222", - matches: ["%25555%25222", "555", "222"], expected: { path: "%25555%25222", params: { foo: "555", bar: "222" }, diff --git a/src/index.spec.ts b/src/index.spec.ts index b51dd9b..ef019c9 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -120,10 +120,8 @@ describe("path-to-regexp", () => { describe.each(MATCH_TESTS)( "match $path with $options", ({ path, options, tests }) => { - it.each(tests)("should match $input", ({ input, matches, expected }) => { + it.each(tests)("should match $input", ({ input, expected }) => { const fn = match(path, options); - - expect(exec(fn.re, input)).toEqual(matches); expect(fn(input)).toEqual(expected); }); }, diff --git a/src/index.ts b/src/index.ts index 9826439..2692df0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -365,23 +365,23 @@ export function isOptional(key: Key) { * Convert a single token into a path building function. */ function keyToFunction( - token: Key, + key: Key, encode: Encode | false, ): (data: ParamData) => string { const encodeValue = encode || NOOP_VALUE; - const { prefix = "", suffix = "", separator = suffix + prefix } = token; + const { prefix = "", suffix = "", separator = suffix + prefix } = key; - if (encode && isRepeat(token)) { + if (encode && isRepeat(key)) { const stringify = (value: string, index: number) => { if (typeof value !== "string") { - throw new TypeError(`Expected "${token.name}/${index}" to be a string`); + throw new TypeError(`Expected "${key.name}/${index}" to be a string`); } return encodeValue(value); }; const compile = (value: unknown) => { if (!Array.isArray(value)) { - throw new TypeError(`Expected "${token.name}" to be an array`); + throw new TypeError(`Expected "${key.name}" to be an array`); } if (value.length === 0) return ""; @@ -389,37 +389,37 @@ function keyToFunction( return prefix + value.map(stringify).join(separator) + suffix; }; - if (isOptional(token)) { + if (isOptional(key)) { return (data): string => { - const value = data[token.name]; + const value = data[key.name]; if (value == null) return ""; return value.length ? compile(value) : ""; }; } return (data): string => { - const value = data[token.name]; + const value = data[key.name]; return compile(value); }; } const stringify = (value: unknown) => { if (typeof value !== "string") { - throw new TypeError(`Expected "${token.name}" to be a string`); + throw new TypeError(`Expected "${key.name}" to be a string`); } return prefix + encodeValue(value) + suffix; }; - if (isOptional(token)) { + if (isOptional(key)) { return (data): string => { - const value = data[token.name]; + const value = data[key.name]; if (value == null) return ""; return stringify(value); }; } return (data): string => { - const value = data[token.name]; + const value = data[key.name]; return stringify(value); }; } @@ -480,18 +480,7 @@ export type Match

= false | MatchResult

; /** * The match function takes a string and returns whether it matched the path. */ -export type MatchFunction

= (( - path: string, -) => Match

) & { re: RegExp }; - -const isEnd = (input: string, match: string) => input.length === match.length; -const isDelimiterOrEnd = - (delimiter: string) => (input: string, match: string) => { - return ( - match.length === input.length || - input.slice(match.length, match.length + delimiter.length) === delimiter - ); - }; +export type MatchFunction

= (path: string) => Match

; /** * Create path match function from `path-to-regexp` spec. @@ -501,18 +490,29 @@ export function $match

( options: MatchOptions = {}, ): MatchFunction

{ const { decode = decodeURIComponent, end = true } = options; - const re = tokensToRegexp(data, options); + const { delimiter } = data; + const keys: Key[] = []; + const flags = toFlags(options); + const sources = toRegExpSource(data, keys); + const re = new RegExp( + `^${sources.join("")}(?=${escape(delimiter)}|$)`, + flags, + ); + + const decoders = keys.map((key) => { + if (!decode) return NOOP_VALUE; - const decoders = re.keys.map((key) => { - if (decode && (key.modifier === "+" || key.modifier === "*")) { + if (isRepeat(key)) { const { prefix = "", suffix = "", separator = suffix + prefix } = key; return (value: string) => value.split(separator).map(decode); } - return decode || NOOP_VALUE; + return decode; }); - const validate = end ? isEnd : isDelimiterOrEnd(data.delimiter); + const isValid = end + ? (a: string, b: string) => a.length === b.length + : () => true; return Object.assign( function match(input: string) { @@ -520,13 +520,13 @@ export function $match

( if (!m) return false; const { 0: path } = m; - if (!validate(input, path)) return false; + if (!isValid(input, path)) return false; const params = Object.create(null); for (let i = 1; i < m.length; i++) { if (m[i] === undefined) continue; - const key = re.keys[i - 1]; + const key = keys[i - 1]; const decoder = decoders[i - 1]; params[key.name] = decoder(m[i]); } @@ -575,17 +575,6 @@ export interface Key { */ export type Token = string | Key; -/** - * Expose a function for taking tokens and returning a RegExp. - */ -function tokensToRegexp(data: TokenData, options: PathOptions) { - const flags = toFlags(options); - const keys: Key[] = []; - const sources = toRegExpSource(data, keys); - const regexp = new RegExp(`^${sources.join("")}`, flags); - return Object.assign(regexp, { keys }); -} - /** * Convert a token into a regexp string (re-used for path validation). */ From 74f97b59728f111bd457646a340f7c2eb8cfec66 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 12 Aug 2024 16:20:55 -0700 Subject: [PATCH 10/12] Create SECURITY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4aa608f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Security contact information + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. From 60f2121e9b66b7b622cc01080df0aabda9eedee6 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 1 Sep 2024 14:56:05 -0700 Subject: [PATCH 11/12] Rewrite and simplify API --- Readme.md | 231 ++------ package.json | 1 + scripts/redos.ts | 8 +- src/cases.spec.ts | 1365 +++++++++++-------------------------------- src/index.bench.ts | 42 ++ src/index.spec.ts | 174 ++---- src/index.ts | 763 +++++++++++------------- tsconfig.build.json | 2 +- 8 files changed, 819 insertions(+), 1767 deletions(-) create mode 100644 src/index.bench.ts diff --git a/Readme.md b/Readme.md index eaa60b2..263c0f7 100644 --- a/Readme.md +++ b/Readme.md @@ -24,29 +24,9 @@ const { match, compile, parse } = require("path-to-regexp"); // parse(path, options?) ``` -### Match - -The `match` function returns a function for transforming paths into parameters: - -- **path** A string. -- **options** _(optional)_ (See [parse](#parse) for more options) - - **sensitive** Regexp will be case sensitive. (default: `false`) - - **end** Validate the match reaches the end of the string. (default: `true`) - - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) - -```js -const fn = match("/foo/:bar"); -``` - -**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). - ### Parameters -Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. - -#### Named parameters - -Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters, similar to JavaScript. +Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. They are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid JavaScript identifier, or be double quoted to use other characters (`:"param-name"`). ```js const fn = match("/:foo/:bar"); @@ -55,137 +35,54 @@ fn("/test/route"); //=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } ``` -##### Custom matching parameters - -Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path: - -```js -const exampleNumbers = match("/icon-:foo(\\d+).png"); - -exampleNumbers("/icon-123.png"); -//=> { path: '/icon-123.png', params: { foo: '123' } } - -exampleNumbers("/icon-abc.png"); -//=> false - -const exampleWord = pathToRegexp("/(user|u)"); - -exampleWord("/u"); -//=> { path: '/u', params: { '0': 'u' } } - -exampleWord("/users"); -//=> false -``` - -**Tip:** Backslashes need to be escaped with another backslash in JavaScript strings. - -#### Unnamed parameters - -It is possible to define a parameter without a name. The name will be numerically indexed: - -```js -const fn = match("/:foo/(.*)"); - -fn("/test/route"); -//=> { path: '/test/route', params: { '0': 'route', foo: 'test' } } -``` - -#### Custom prefix and suffix - -Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: - -```js -const fn = match("{/:attr1}?{-:attr2}?{-:attr3}?"); - -fn("/test"); -//=> { path: '/test', params: { attr1: 'test' } } - -fn("/test-test"); -//=> { path: '/test-test', params: { attr1: 'test', attr2: 'test' } } -``` - -#### Modifiers - -Modifiers are used after parameters with custom prefixes and suffixes (`{}`). - -##### Optional - -Parameters can be suffixed with a question mark (`?`) to make the parameter optional. - -```js -const fn = match("/:foo{/:bar}?"); - -fn("/test"); -//=> { path: '/test', params: { foo: 'test' } } - -fn("/test/route"); -//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } -``` - -##### Zero or more +### Wildcard -Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches. +Wildcard parameters match one or more characters across multiple segments. They are defined the same way as regular parameters, but are prefixed with an asterisk (`*foo`). ```js -const fn = match("{/:foo}*"); - -fn("/foo"); -//=> { path: '/foo', params: { foo: [ 'foo' ] } } +const fn = match("/*splat"); fn("/bar/baz"); -//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } +//=> { path: '/bar/baz', params: { splat: [ 'bar', 'baz' ] } } ``` -##### One or more +### Optional -Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches. +Braces can be used to define parts of the path that are optional. ```js -const fn = match("{/:foo}+"); +const fn = match("/users{/:id}/delete"); -fn("/"); -//=> false +fn("/users/delete"); +//=> { path: '/users/delete', params: {} } -fn("/bar/baz"); -//=> { path: '/bar/baz', params: { foo: [ 'bar', 'baz' ] } } +fn("/users/123/delete"); +//=> { path: '/users/123/delete', params: { id: '123' } } ``` -##### Custom separator - -By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this: - -```js -const fn = match("/name{/:parts;-}+"); +## Match -fn("/name"); -//=> false +The `match` function returns a function for matching strings against a path: -fn("/bar/1-2-3"); -//=> { path: '/name/1-2-3', params: { parts: [ '1', '2', '3' ] } } -``` - -#### Wildcard - -A wildcard is also supported. It is roughly equivalent to `(.*)`. +- **path** String or array of strings. +- **options** _(optional)_ (See [parse](#parse) for more options) + - **sensitive** Regexp will be case sensitive. (default: `false`) + - **end** Validate the match reaches the end of the string. (default: `true`) + - **trailing** Allows optional trailing delimiter to match. (default: `true`) + - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) ```js -const fn = match("/*"); - -fn("/"); -//=> { path: '/', params: {} } - -fn("/bar/baz"); -//=> { path: '/bar/baz', params: { '0': [ 'bar', 'baz' ] } } +const fn = match("/foo/:bar"); ``` -### Compile ("Reverse" Path-To-RegExp) +**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). + +## Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: - **path** A string. - **options** (See [parse](#parse) for more options) - - **sensitive** Regexp will be case sensitive. (default: `false`) - - **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`) - **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) ```js @@ -194,46 +91,34 @@ const toPath = compile("/user/:id"); toPath({ id: "name" }); //=> "/user/name" toPath({ id: "café" }); //=> "/user/caf%C3%A9" -// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. -const toPathRaw = compile("/user/:id", { encode: false }); - -toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" -toPathRaw({ id: ":/" }); //=> Throws, "/user/:/" when `validate` is `false`. - -const toPathRepeated = compile("{/:segment}+"); +const toPathRepeated = compile("/*segment"); toPathRepeated({ segment: ["foo"] }); //=> "/foo" toPathRepeated({ segment: ["a", "b", "c"] }); //=> "/a/b/c" -const toPathRegexp = compile("/user/:id(\\d+)"); +// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. +const toPathRaw = compile("/user/:id", { encode: false }); -toPathRegexp({ id: "123" }); //=> "/user/123" +toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" ``` ## Developers - If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around. -- To ensure matches work on paths containing characters usually encoded, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. +- To ensure matches work on paths containing characters usually encoded, such as emoji, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. ### Parse -The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `$match` and `$compile`. +The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `match` and `compile`. - **path** A string. - **options** _(optional)_ - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding) + - **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl)) ### Tokens -The `tokens` returned by `TokenData` is an array of strings or keys, represented as objects, with the following properties: - -- `name` The name of the token -- `prefix` _(optional)_ The prefix string for the segment (e.g. `"/"`) -- `suffix` _(optional)_ The suffix string for the segment (e.g. `""`) -- `pattern` _(optional)_ The pattern defined to match this token -- `modifier` _(optional)_ The modifier character used for the segment (e.g. `?`) -- `separator` _(optional)_ The string used to separate repeated parameters +`TokenData` is a sequence of tokens, currently of types `text`, `parameter`, `wildcard`, or `group`. ### Custom path @@ -242,9 +127,12 @@ In some applications, you may not be able to use the `path-to-regexp` syntax, bu ```js import { TokenData, match } from "path-to-regexp"; -const tokens = ["/", { name: "foo" }]; -const path = new TokenData(tokens, "/"); -const fn = $match(path); +const tokens = [ + { type: "text", value: "/" }, + { type: "parameter", name: "foo" }, +]; +const path = new TokenData(tokens); +const fn = match(path); fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } ``` @@ -253,55 +141,34 @@ fn("/test"); //=> { path: '/test', index: 0, params: { foo: 'test' } } An effort has been made to ensure ambiguous paths from previous releases throw an error. This means you might be seeing an error when things worked before. -### Unexpected `?`, `*`, or `+` - -In previous major versions `/` and `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. For example: - -- `/:key?` → `{/:key}?` or `/:key*` → `{/:key}*` or `/:key+` → `{/:key}+` -- `.:key?` → `{.:key}?` or `.:key*` → `{.:key}*` or `.:key+` → `{.:key}+` -- `:key?` → `{:key}?` or `:key*` → `{:key}*` or `:key+` → `{:key}+` +### Unexpected `?` or `+` -### Unexpected `;` +In past releases, `?`, `*`, and `+` were used to denote optional or repeating parameters. As an alternative, try these: -Used as a [custom separator](#custom-separator) for repeated parameters. +- For optional (`?`), use an empty segment in a group such as `/:file{.:ext}`. +- For repeating (`+`), only wildcard matching is supported, such as `/*path`. +- For optional repeating (`*`), use a group and a wildcard parameter such as `/files{/*path}`. -### Unexpected `!`, `@`, or `,` +### Unexpected `(`, `)`, `[`, `]`, etc. -These characters have been reserved for future use. - -### Missing separator - -Repeated parameters must have a separator to be valid. For example, `{:foo}*` can't be used. Separators can be defined manually, such as `{:foo;/}*`, or they default to the suffix and prefix with the parameter, such as `{/:foo}*`. +Previous versions of Path-to-RegExp used these for RegExp features. This version no longer supports them so they've been reserved to avoid ambiguity. To use these characters literally, escape them with a backslash, e.g. `"\\("`. ### Missing parameter name -Parameter names, the part after `:`, must be a valid JavaScript identifier. For example, it cannot start with a number or dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`. +Parameter names, the part after `:` or `*`, must be a valid JavaScript identifier. For example, it cannot start with a number or contain a dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`. ### Unterminated quote Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character. -### Pattern cannot start with "?" - -Parameters in `path-to-regexp` must be basic groups. However, you can use features that require the `?` nested within the pattern. For example, `:foo((?!login)[^/]+)` is valid, but `:foo(?!login)` is not. - -### Capturing groups are not allowed - -A parameter pattern can not contain nested capturing groups. - -### Unbalanced or missing pattern - -A parameter pattern must have the expected number of parentheses. An unbalanced amount, such as `((?!login)` implies something has been written that is invalid. Check you didn't forget any parentheses. - ### Express <= 4.x Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways: -- The only part of the string that is a regex is within `()`. - - In Express.js 4.x, everything was passed as-is after a simple replacement, so you could write `/[a-z]+` to match `/test`. -- The `?` optional character must be used after `{}`. +- Regexp characters can no longer be provided. +- The optional character `?` is no longer supported, use braces instead: `/:file{.:ext}`. - Some characters have new meaning or have been reserved (`{}?*+@!;`). -- The parameter name now supports all unicode identifier characters, previously it was only `[a-z0-9]`. +- The parameter name now supports all JavaScript identifier characters, previously it was only `[a-z0-9]`. ## License diff --git a/package.json b/package.json index e1c220a..a57212a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dist/" ], "scripts": { + "bench": "vitest bench", "build": "ts-scripts build", "format": "ts-scripts format", "lint": "ts-scripts lint", diff --git a/scripts/redos.ts b/scripts/redos.ts index f83e52d..841cd07 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -5,13 +5,7 @@ import { MATCH_TESTS } from "../src/cases.spec.js"; let safe = 0; let fail = 0; -const TESTS = new Set(MATCH_TESTS.map((test) => test.path)); -// const TESTS = [ -// ":path([^\\.]+).:ext", -// ":path.:ext(\\w+)", -// ":path{.:ext([^\\.]+)}", -// "/:path.:ext(\\\\w+)", -// ]; +const TESTS = MATCH_TESTS.map((x) => x.path); for (const path of TESTS) { const { re } = match(path) as any; diff --git a/src/cases.spec.ts b/src/cases.spec.ts index 508b946..ef06e1f 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -34,31 +34,56 @@ export interface MatchTestSet { export const PARSER_TESTS: ParserTestSet[] = [ { path: "/", - expected: ["/"], + expected: [{ type: "text", value: "/" }], }, { path: "/:test", - expected: ["/", { name: "test" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "test" }, + ], }, { path: '/:"0"', - expected: ["/", { name: "0" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "0" }, + ], }, { path: "/:_", - expected: ["/", { name: "_" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "_" }, + ], }, { path: "/:café", - expected: ["/", { name: "café" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "café" }, + ], }, { path: '/:"123"', - expected: ["/", { name: "123" }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: "123" }, + ], }, { path: '/:"1\\"\\2\\"3"', - expected: ["/", { name: '1"2"3' }], + expected: [ + { type: "text", value: "/" }, + { type: "param", name: '1"2"3' }, + ], + }, + { + path: "/*path", + expected: [ + { type: "text", value: "/" }, + { type: "wildcard", name: "path" }, + ], }, ]; @@ -106,7 +131,6 @@ export const COMPILE_TESTS: CompileTestSet[] = [ }, { path: "/:test", - options: { validate: false }, tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -116,7 +140,7 @@ export const COMPILE_TESTS: CompileTestSet[] = [ }, { path: "/:test", - options: { validate: false, encode: false }, + options: { encode: false }, tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, @@ -124,16 +148,6 @@ export const COMPILE_TESTS: CompileTestSet[] = [ { input: { test: "123/xyz" }, expected: "/123/xyz" }, ], }, - { - path: "/:test", - options: { encode: encodeURIComponent }, - tests: [ - { input: undefined, expected: null }, - { input: {}, expected: null }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123%2Fxyz" }, - ], - }, { path: "/:test", options: { encode: () => "static" }, @@ -145,56 +159,32 @@ export const COMPILE_TESTS: CompileTestSet[] = [ ], }, { - path: "{/:test}?", + path: "{/:test}", options: { encode: false }, tests: [ { input: undefined, expected: "" }, { input: {}, expected: "" }, { input: { test: undefined }, expected: "" }, { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, + { input: { test: "123/xyz" }, expected: "/123/xyz" }, ], }, { - path: "/:test(.*)", - options: { encode: false }, + path: "/*test", tests: [ { input: undefined, expected: null }, { input: {}, expected: null }, - { input: { test: "" }, expected: "/" }, - { input: { test: "123" }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: "/123/xyz" }, - ], - }, - { - path: "{/:test}*", - tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: [] }, expected: "" }, - { input: { test: [""] }, expected: null }, + { input: { test: [] }, expected: null }, { input: { test: ["123"] }, expected: "/123" }, - { input: { test: "123/xyz" }, expected: null }, { input: { test: ["123", "xyz"] }, expected: "/123/xyz" }, ], }, { - path: "{/:test}*", + path: "/*test", options: { encode: false }, tests: [ - { input: undefined, expected: "" }, - { input: {}, expected: "" }, - { input: { test: "" }, expected: null }, { input: { test: "123" }, expected: "/123" }, { input: { test: "123/xyz" }, expected: "/123/xyz" }, - { input: { test: ["123", "xyz"] }, expected: null }, - ], - }, - { - path: "/{<:foo>}+", - tests: [ - { input: undefined, expected: null }, - { input: { foo: ["x", "y", "z"] }, expected: "/" }, ], }, ]; @@ -227,7 +217,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test/route", expected: false }, { input: "/test/", - expected: false, + expected: { path: "/test/", params: {} }, }, ], }, @@ -242,7 +232,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/test", expected: false }, { input: "/test//", - expected: false, + expected: { path: "/test//", params: {} }, }, ], }, @@ -255,7 +245,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route/", - expected: false, + expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route.json", @@ -266,7 +256,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route.json/", - expected: false, + expected: { + path: "/route.json/", + params: { test: "route.json" }, + }, }, { input: "/route/test", @@ -341,7 +334,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test/", - expected: { path: "/test", params: {} }, + expected: { path: "/test/", params: {} }, }, { input: "/test////", @@ -377,7 +370,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/test//", - expected: { path: "/test/", params: {} }, + expected: { path: "/test//", params: {} }, }, { input: "/test/route", @@ -401,7 +394,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/route/", - expected: { path: "/route", params: { test: "route" } }, + expected: { path: "/route/", params: { test: "route" } }, }, { input: "/route.json", @@ -413,7 +406,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ { input: "/route.json/", expected: { - path: "/route.json", + path: "/route.json/", params: { test: "route.json" }, }, }, @@ -477,7 +470,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/", - expected: { path: "", params: {} }, + expected: { path: "/", params: {} }, }, { input: "route", @@ -498,7 +491,28 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Optional. */ { - path: "{/:test}?", + path: "{/route}", + tests: [ + { + input: "", + expected: { path: "", params: {} }, + }, + { + input: "/", + expected: { path: "/", params: {} }, + }, + { + input: "/foo", + expected: false, + }, + { + input: "/route", + expected: { path: "/route", params: {} }, + }, + ], + }, + { + path: "{/:test}", tests: [ { input: "/route", @@ -510,12 +524,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/", - expected: false, + expected: { path: "/", params: {} }, }, ], }, { - path: "{/:test}?/bar", + path: "{/:test}/bar", tests: [ { input: "/bar", @@ -527,12 +541,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo/bar/", - expected: false, + expected: { path: "/foo/bar/", params: { test: "foo" } }, }, ], }, { - path: "{/:test}?-bar", + path: "{/:test}-bar", tests: [ { input: "-bar", @@ -544,12 +558,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo-bar/", - expected: false, + expected: { path: "/foo-bar/", params: { test: "foo" } }, }, ], }, { - path: "/{:test}?-bar", + path: "/{:test}-bar", tests: [ { input: "/-bar", @@ -561,810 +575,266 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/foo-bar/", - expected: false, + expected: { path: "/foo-bar/", params: { test: "foo" } }, }, ], }, /** - * Zero or more times. + * No prefix characters. */ { - path: "{/:test}*", + path: "test", tests: [ { - input: "/", - expected: false, + input: "test", + expected: { path: "test", params: {} }, }, { - input: "//", + input: "/test", expected: false, }, - { - input: "/route", - expected: { path: "/route", params: { test: ["route"] } }, - }, - { - input: "/some/basic/route", - expected: { - path: "/some/basic/route", - params: { test: ["some", "basic", "route"] }, - }, - }, ], }, { - path: "{/:test}*-bar", + path: ":test", tests: [ { - input: "-bar", - expected: { path: "-bar", params: {} }, + input: "route", + expected: { path: "route", params: { test: "route" } }, }, { - input: "/-bar", + input: "/route", expected: false, }, { - input: "/foo-bar", - expected: { path: "/foo-bar", params: { test: ["foo"] } }, + input: "route/", + expected: { path: "route/", params: { test: "route" } }, + }, + ], + }, + { + path: "{:test}", + tests: [ + { + input: "test", + expected: { path: "test", params: { test: "test" } }, }, { - input: "/foo/baz-bar", - expected: { - path: "/foo/baz-bar", - params: { test: ["foo", "baz"] }, - }, + input: "", + expected: { path: "", params: {} }, }, ], }, /** - * One or more times. + * Formats. */ { - path: "{/:test}+", + path: "/test.json", tests: [ { - input: "/", - expected: false, + input: "/test.json", + expected: { path: "/test.json", params: {} }, }, { - input: "//", + input: "/test", expected: false, }, - { - input: "/route", - expected: { path: "/route", params: { test: ["route"] } }, - }, - { - input: "/some/basic/route", - expected: { - path: "/some/basic/route", - params: { test: ["some", "basic", "route"] }, - }, - }, ], }, { - path: "{/:test}+-bar", + path: "/:test.json", tests: [ { - input: "-bar", + input: "/.json", expected: false, }, { - input: "/-bar", - expected: false, + input: "/test.json", + expected: { path: "/test.json", params: { test: "test" } }, }, { - input: "/foo-bar", - expected: { path: "/foo-bar", params: { test: ["foo"] } }, + input: "/route.json", + expected: { path: "/route.json", params: { test: "route" } }, }, { - input: "/foo/baz-bar", - expected: { - path: "/foo/baz-bar", - params: { test: ["foo", "baz"] }, - }, + input: "/route.json.json", + expected: { path: "/route.json.json", params: { test: "route.json" } }, }, ], }, /** - * Custom parameters. + * Format and path params. */ { - path: String.raw`/:test(\d+)`, + path: "/:test.:format", tests: [ { - input: "/123", - expected: { path: "/123", params: { test: "123" } }, + input: "/route.html", + expected: { + path: "/route.html", + params: { test: "route", format: "html" }, + }, }, { - input: "/abc", + input: "/route", expected: false, }, { - input: "/123/abc", - expected: false, + input: "/route.html.json", + expected: { + path: "/route.html.json", + params: { test: "route.html", format: "json" }, + }, }, ], }, { - path: String.raw`/:test(\d+)-bar`, + path: "/:test{.:format}", tests: [ { - input: "-bar", - expected: false, + input: "/route", + expected: { path: "/route", params: { test: "route" } }, }, { - input: "/-bar", - expected: false, + input: "/route.json", + expected: { + path: "/route.json", + params: { test: "route", format: "json" }, + }, }, { - input: "/abc-bar", - expected: false, + input: "/route.json.html", + expected: { + path: "/route.json.html", + params: { test: "route.json", format: "html" }, + }, }, + ], + }, + { + path: "/:test.:format\\z", + tests: [ { - input: "/123-bar", - expected: { path: "/123-bar", params: { test: "123" } }, + input: "/route.htmlz", + expected: { + path: "/route.htmlz", + params: { test: "route", format: "html" }, + }, }, { - input: "/123/456-bar", + input: "/route.html", expected: false, }, ], }, + + /** + * Escaped characters. + */ { - path: "/:test(.*)", + path: "/\\(testing\\)", tests: [ { - input: "/", - expected: { path: "/", params: { test: "" } }, + input: "/testing", + expected: false, }, { - input: "/route", - expected: { path: "/route", params: { test: "route" } }, + input: "/(testing)", + expected: { path: "/(testing)", params: {} }, }, + ], + }, + { + path: "/.\\+\\*\\?\\{\\}=^\\!\\:$\\[\\]\\|", + tests: [ { - input: "/route/123", - expected: { - path: "/route/123", - params: { test: "route/123" }, - }, + input: "/.+*?{}=^!:$[]|", + expected: { path: "/.+*?{}=^!:$[]|", params: {} }, }, + ], + }, + + /** + * Random examples. + */ + { + path: "/:foo/:bar", + tests: [ { - input: "/;,:@&=/+$-_.!/~*()", + input: "/match/route", expected: { - path: "/;,:@&=/+$-_.!/~*()", - params: { test: ";,:@&=/+$-_.!/~*()" }, + path: "/match/route", + params: { foo: "match", bar: "route" }, }, }, ], }, { - path: "/:test([a-z]+)", + path: "/:foo\\(test\\)/bar", tests: [ { - input: "/abc", - expected: { path: "/abc", params: { test: "abc" } }, - }, - { - input: "/123", - expected: false, + input: "/foo(test)/bar", + expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, }, { - input: "/abc/123", + input: "/foo/bar", expected: false, }, ], }, { - path: "/:test(this|that)", + path: "/:foo\\?", tests: [ { - input: "/this", - expected: { path: "/this", params: { test: "this" } }, - }, - { - input: "/that", - expected: { path: "/that", params: { test: "that" } }, + input: "/route?", + expected: { path: "/route?", params: { foo: "route" } }, }, { - input: "/foo", + input: "/route", expected: false, }, ], }, { - path: "{/:test(abc|xyz)}*", + path: "/{:pre}baz", tests: [ { - input: "/", - expected: false, + input: "/foobaz", + expected: { path: "/foobaz", params: { pre: "foo" } }, }, { - input: "/abc", - expected: { path: "/abc", params: { test: ["abc"] } }, + input: "/baz", + expected: { path: "/baz", params: { pre: undefined } }, }, + ], + }, + { + path: "/:foo\\(:bar\\)", + tests: [ { - input: "/abc/abc", + input: "/hello(world)", expected: { - path: "/abc/abc", - params: { test: ["abc", "abc"] }, + path: "/hello(world)", + params: { foo: "hello", bar: "world" }, }, }, { - input: "/xyz/xyz", - expected: { - path: "/xyz/xyz", - params: { test: ["xyz", "xyz"] }, - }, + input: "/hello()", + expected: false, }, + ], + }, + { + path: "/:foo\\({:bar}\\)", + tests: [ { - input: "/abc/xyz", + input: "/hello(world)", expected: { - path: "/abc/xyz", - params: { test: ["abc", "xyz"] }, - }, - }, - { - input: "/abc/xyz/abc/xyz", - expected: { - path: "/abc/xyz/abc/xyz", - params: { test: ["abc", "xyz", "abc", "xyz"] }, - }, - }, - { - input: "/xyzxyz", - expected: false, - }, - ], - }, - - /** - * No prefix characters. - */ - { - path: "test", - tests: [ - { - input: "test", - expected: { path: "test", params: {} }, - }, - { - input: "/test", - expected: false, - }, - ], - }, - { - path: ":test", - tests: [ - { - input: "route", - expected: { path: "route", params: { test: "route" } }, - }, - { - input: "/route", - expected: false, - }, - { - input: "route/", - expected: false, - }, - ], - }, - { - path: "{:test}?", - tests: [ - { - input: "test", - expected: { path: "test", params: { test: "test" } }, - }, - { - input: "", - expected: { path: "", params: {} }, - }, - ], - }, - { - path: "{:test/}+", - tests: [ - { - input: "route/", - expected: { path: "route/", params: { test: ["route"] } }, - }, - { - input: "/route", - expected: false, - }, - { - input: "", - expected: false, - }, - { - input: "foo/bar/", - expected: { - path: "foo/bar/", - params: { test: ["foo", "bar"] }, - }, - }, - ], - }, - - /** - * Formats. - */ - { - path: "/test.json", - tests: [ - { - input: "/test.json", - expected: { path: "/test.json", params: {} }, - }, - { - input: "/test", - expected: false, - }, - ], - }, - { - path: "/:test.json", - tests: [ - { - input: "/.json", - expected: false, - }, - { - input: "/test.json", - expected: { path: "/test.json", params: { test: "test" } }, - }, - { - input: "/route.json", - expected: { path: "/route.json", params: { test: "route" } }, - }, - { - input: "/route.json.json", - expected: false, - }, - ], - }, - { - path: "/:test([^/]+).json", - tests: [ - { - input: "/route.json.json", - expected: { - path: "/route.json.json", - params: { test: "route.json" }, - }, - }, - ], - }, - - /** - * Format params. - */ - { - path: "/test.:format(\\w+)", - tests: [ - { - input: "/test.html", - expected: { path: "/test.html", params: { format: "html" } }, - }, - { - input: "/test", - expected: false, - }, - ], - }, - { - path: "/test.:format(\\w+).:format(\\w+)", - tests: [ - { - input: "/test.html.json", - expected: { - path: "/test.html.json", - params: { format: "json" }, - }, - }, - { - input: "/test.html", - expected: false, - }, - ], - }, - { - path: "/test{.:format(\\w+)}?", - tests: [ - { - input: "/test", - expected: { path: "/test", params: { format: undefined } }, - }, - { - input: "/test.html", - expected: { path: "/test.html", params: { format: "html" } }, - }, - ], - }, - { - path: "/test{.:format(\\w+)}+", - tests: [ - { - input: "/test", - expected: false, - }, - { - input: "/test.html", - expected: { - path: "/test.html", - params: { format: ["html"] }, - }, - }, - { - input: "/test.html.json", - expected: { - path: "/test.html.json", - params: { format: ["html", "json"] }, - }, - }, - ], - }, - { - path: "/test{.:format}+", - tests: [ - { - input: "/test", - expected: false, - }, - { - input: "/test.html", - expected: { - path: "/test.html", - params: { format: ["html"] }, - }, - }, - { - input: "/test.hbs.html", - expected: { - path: "/test.hbs.html", - params: { format: ["hbs", "html"] }, - }, - }, - ], - }, - - /** - * Format and path params. - */ - { - path: "/:test.:format", - tests: [ - { - input: "/route.html", - expected: { - path: "/route.html", - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route", - expected: false, - }, - { - input: "/route.html.json", - expected: { - path: "/route.html.json", - params: { test: "route", format: "html.json" }, - }, - }, - ], - }, - { - path: "/:test{.:format}?", - tests: [ - { - input: "/route", - expected: { path: "/route", params: { test: "route" } }, - }, - { - input: "/route.json", - expected: { - path: "/route.json", - params: { test: "route", format: "json" }, - }, - }, - { - input: "/route.json.html", - expected: { - path: "/route.json.html", - params: { test: "route", format: "json.html" }, - }, - }, - ], - }, - { - path: "/:test.:format\\z", - tests: [ - { - input: "/route.htmlz", - expected: { - path: "/route.htmlz", - params: { test: "route", format: "html" }, - }, - }, - { - input: "/route.html", - expected: false, - }, - ], - }, - - /** - * Unnamed params. - */ - { - path: "/(\\d+)", - tests: [ - { - input: "/123", - expected: { path: "/123", params: { "0": "123" } }, - }, - { - input: "/abc", - expected: false, - }, - { - input: "/123/abc", - expected: false, - }, - ], - }, - { - path: "{/(\\d+)}?", - tests: [ - { - input: "/", - expected: false, - }, - { - input: "/123", - expected: { path: "/123", params: { "0": "123" } }, - }, - ], - }, - { - path: "/route\\(\\\\(\\d+\\\\)\\)", - tests: [ - { - input: "/route(\\123\\)", - expected: { - path: "/route(\\123\\)", - params: { "0": "123\\" }, - }, - }, - { - input: "/route(\\123)", - expected: false, - }, - ], - }, - { - path: "{/route}?", - tests: [ - { - input: "", - expected: { path: "", params: {} }, - }, - { - input: "/", - expected: false, - }, - { - input: "/foo", - expected: false, - }, - { - input: "/route", - expected: { path: "/route", params: {} }, - }, - ], - }, - { - path: "{/(.*)}", - tests: [ - { - input: "/", - expected: { path: "/", params: { "0": "" } }, - }, - { - input: "/login", - expected: { path: "/login", params: { "0": "login" } }, - }, - ], - }, - - /** - * Escaped characters. - */ - { - path: "/\\(testing\\)", - tests: [ - { - input: "/testing", - expected: false, - }, - { - input: "/(testing)", - expected: { path: "/(testing)", params: {} }, - }, - ], - }, - { - path: "/.\\+\\*\\?\\{\\}=^\\!\\:$[]\\|", - tests: [ - { - input: "/.+*?{}=^!:$[]|", - expected: { path: "/.+*?{}=^!:$[]|", params: {} }, - }, - ], - }, - { - path: "/test/{:uid(u\\d+)}?{:cid(c\\d+)}?", - tests: [ - { - input: "/test/u123", - expected: { path: "/test/u123", params: { uid: "u123" } }, - }, - { - input: "/test/c123", - expected: { path: "/test/c123", params: { cid: "c123" } }, - }, - ], - }, - - /** - * Unnamed group prefix. - */ - { - path: "/{apple-}?icon-:res(\\d+).png", - tests: [ - { - input: "/icon-240.png", - expected: { path: "/icon-240.png", params: { res: "240" } }, - }, - { - input: "/apple-icon-240.png", - expected: { - path: "/apple-icon-240.png", - params: { res: "240" }, - }, - }, - ], - }, - - /** - * Random examples. - */ - { - path: "/:foo/:bar", - tests: [ - { - input: "/match/route", - expected: { - path: "/match/route", - params: { foo: "match", bar: "route" }, - }, - }, - ], - }, - { - path: "/:foo\\(test\\)/bar", - tests: [ - { - input: "/foo(test)/bar", - expected: { path: "/foo(test)/bar", params: { foo: "foo" } }, - }, - { - input: "/foo/bar", - expected: false, - }, - ], - }, - { - path: "/:remote([\\w\\-\\.]+)/:user([\\w-]+)", - tests: [ - { - input: "/endpoint/user", - expected: { - path: "/endpoint/user", - params: { remote: "endpoint", user: "user" }, - }, - }, - { - input: "/endpoint/user-name", - expected: { - path: "/endpoint/user-name", - params: { remote: "endpoint", user: "user-name" }, - }, - }, - { - input: "/foo.bar/user-name", - expected: { - path: "/foo.bar/user-name", - params: { remote: "foo.bar", user: "user-name" }, - }, - }, - ], - }, - { - path: "/:foo\\?", - tests: [ - { - input: "/route?", - expected: { path: "/route?", params: { foo: "route" } }, - }, - { - input: "/route", - expected: false, - }, - ], - }, - { - path: "{/:foo}+bar", - tests: [ - { - input: "/foobar", - expected: { path: "/foobar", params: { foo: ["foo"] } }, - }, - { - input: "/foo/bar", - expected: false, - }, - { - input: "/foo/barbar", - expected: false, - }, - ], - }, - { - path: "/{:pre}?baz", - tests: [ - { - input: "/foobaz", - expected: { path: "/foobaz", params: { pre: "foo" } }, - }, - { - input: "/baz", - expected: { path: "/baz", params: { pre: undefined } }, - }, - ], - }, - { - path: "/:foo\\(:bar\\)", - tests: [ - { - input: "/hello(world)", - expected: { - path: "/hello(world)", - params: { foo: "hello", bar: "world" }, - }, - }, - { - input: "/hello()", - expected: false, - }, - ], - }, - { - path: "/:foo\\({:bar}?\\)", - tests: [ - { - input: "/hello(world)", - expected: { - path: "/hello(world)", - params: { foo: "hello", bar: "world" }, + path: "/hello(world)", + params: { foo: "hello", bar: "world" }, }, }, { @@ -1377,27 +847,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:postType(video|audio|text){(\\+.+)}?", - tests: [ - { - input: "/video", - expected: { path: "/video", params: { postType: "video" } }, - }, - { - input: "/video+test", - expected: { - path: "/video+test", - params: { 0: "+test", postType: "video" }, - }, - }, - { - input: "/video+", - expected: false, - }, - ], - }, - { - path: "{/:foo}?{/:bar}?-ext", + path: "{/:foo}{/:bar}-ext", tests: [ { input: "/-ext", @@ -1428,7 +878,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:required{/:optional}?-ext", + path: "/:required{/:optional}-ext", tests: [ { input: "/foo-ext", @@ -1545,7 +995,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "mail{.:domain}?.com", + path: "mail{.:domain}.com", options: { delimiter: ".", }, @@ -1608,7 +1058,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Prefixes. */ { - path: "{$:foo}{$:bar}?", + path: "$:foo{$:bar}", tests: [ { input: "$x", @@ -1621,20 +1071,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "{$:foo}+", - tests: [ - { - input: "$x", - expected: { path: "$x", params: { foo: ["x"] } }, - }, - { - input: "$x$y", - expected: { path: "$x$y", params: { foo: ["x", "y"] } }, - }, - ], - }, - { - path: "name{/:attr1}?{-:attr2}?{-:attr3}?", + path: "name{/:attr1}{-:attr2}{-:attr3}", tests: [ { input: "name", @@ -1678,108 +1115,12 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, ], }, - { - path: "name{/:attrs;-}*", - tests: [ - { - input: "name", - expected: { path: "name", params: {} }, - }, - { - input: "name/1", - expected: { - path: "name/1", - params: { attrs: ["1"] }, - }, - }, - { - input: "name/1-2", - expected: { - path: "name/1-2", - params: { attrs: ["1", "2"] }, - }, - }, - { - input: "name/1-2-3", - expected: { - path: "name/1-2-3", - params: { attrs: ["1", "2", "3"] }, - }, - }, - { - input: "name/foo-bar/route", - expected: false, - }, - { - input: "name/test/route", - expected: false, - }, - ], - }, - - /** - * Nested parentheses. - */ - { - path: "/:test(\\d+(?:\\.\\d+)?)", - tests: [ - { - input: "/123", - expected: { path: "/123", params: { test: "123" } }, - }, - { - input: "/abc", - expected: false, - }, - { - input: "/123/abc", - expected: false, - }, - { - input: "/123.123", - expected: { path: "/123.123", params: { test: "123.123" } }, - }, - { - input: "/123.abc", - expected: false, - }, - ], - }, - { - path: "/:test((?!login)[^/]+)", - tests: [ - { - input: "/route", - expected: { path: "/route", params: { test: "route" } }, - }, - { - input: "/login", - expected: false, - }, - ], - }, /** * https://github.com/pillarjs/path-to-regexp/issues/206 */ { - path: "/user{(s)}?/:user", - tests: [ - { - input: "/user/123", - expected: { path: "/user/123", params: { user: "123" } }, - }, - { - input: "/users/123", - expected: { - path: "/users/123", - params: { 0: "s", user: "123" }, - }, - }, - ], - }, - { - path: "/user{s}?/:user", + path: "/user{s}/:user", tests: [ { input: "/user/123", @@ -1793,250 +1134,176 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, /** - * https://github.com/pillarjs/path-to-regexp/pull/270 - */ - { - path: "/files{/:path}*{.:ext}*", - tests: [ - { - input: "/files/hello/world.txt", - expected: { - path: "/files/hello/world.txt", - params: { path: ["hello", "world"], ext: ["txt"] }, - }, - }, - { - input: "/files/hello/world.txt.png", - expected: { - path: "/files/hello/world.txt.png", - params: { path: ["hello", "world"], ext: ["txt", "png"] }, - }, - }, - { - input: "/files/my/photo.jpg/gif", - expected: false, - }, - ], - }, - { - path: "/files{/:path}*{.:ext}?", - tests: [ - { - input: "/files/hello/world.txt", - expected: { - path: "/files/hello/world.txt", - params: { path: ["hello", "world"], ext: "txt" }, - }, - }, - { - input: "/files/my/photo.jpg/gif", - expected: false, - }, - ], - }, - { - path: "#/*", - tests: [ - { - input: "#/", - expected: { path: "#/", params: {} }, - }, - ], - }, - { - path: "/foo{/:bar}*", - tests: [ - { - input: "/foo/test1/test2", - expected: { - path: "/foo/test1/test2", - params: { bar: ["test1", "test2"] }, - }, - }, - ], - }, - { - path: "/entity/:id/*", - tests: [ - { - input: "/entity/foo", - expected: false, - }, - { - input: "/entity/foo/", - expected: { path: "/entity/foo/", params: { id: "foo" } }, - }, - ], - }, - { - path: "/test/*", - tests: [ - { - input: "/test", - expected: false, - }, - { - input: "/test/", - expected: { path: "/test/", params: {} }, - }, - { - input: "/test/route", - expected: { path: "/test/route", params: { "0": ["route"] } }, - }, - { - input: "/test/route/nested", - expected: { - path: "/test/route/nested", - params: { "0": ["route", "nested"] }, - }, - }, - ], - }, - - /** - * Asterisk wildcard. + * Wildcard. */ { - path: "/*", + path: "/*path", tests: [ { input: "/", - expected: { path: "/", params: { "0": undefined } }, + expected: false, }, { input: "/route", - expected: { path: "/route", params: { "0": ["route"] } }, + expected: { path: "/route", params: { path: ["route"] } }, }, { input: "/route/nested", expected: { path: "/route/nested", - params: { "0": ["route", "nested"] }, + params: { path: ["route", "nested"] }, }, }, ], }, { - path: "*", + path: "*path", tests: [ { input: "/", - expected: { path: "/", params: { "0": ["", ""] } }, + expected: { path: "/", params: { path: ["", ""] } }, }, { input: "/test", - expected: { path: "/test", params: { "0": ["", "test"] } }, + expected: { path: "/test", params: { path: ["", "test"] } }, }, ], }, { - path: "*", + path: "*path", options: { decode: false }, tests: [ { input: "/", - expected: { path: "/", params: { "0": "/" } }, + expected: { path: "/", params: { path: "/" } }, }, { input: "/test", - expected: { path: "/test", params: { "0": "/test" } }, + expected: { path: "/test", params: { path: "/test" } }, }, ], }, { - path: "/*.:ext", + path: "/*path.:ext", tests: [ { input: "/test.html", expected: { path: "/test.html", - params: { "0": ["test"], ext: "html" }, + params: { path: ["test"], ext: "html" }, }, }, { input: "/test.html/nested", expected: false, }, + { + input: "/test.html/nested.json", + expected: { + path: "/test.html/nested.json", + params: { path: ["test.html", "nested"], ext: "json" }, + }, + }, ], }, { - path: "/*{.:ext}?", + path: "/:path.*ext", tests: [ { input: "/test.html", expected: { path: "/test.html", - params: { "0": ["test.html"], ext: undefined }, + params: { path: "test", ext: ["html"] }, }, }, { input: "/test.html/nested", expected: { - params: { - "0": ["test.html", "nested"], - }, path: "/test.html/nested", + params: { path: "test", ext: ["html", "nested"] }, + }, + }, + { + input: "/test.html/nested.json", + expected: { + path: "/test.html/nested.json", + params: { path: "test", ext: ["html", "nested.json"] }, }, }, ], }, { - path: "/*{.:ext}*", + path: "/*path{.:ext}", tests: [ { input: "/test.html", expected: { path: "/test.html", - params: { "0": ["test.html"], ext: undefined }, + params: { path: ["test"], ext: "html" }, }, }, { input: "/test.html/nested", expected: { params: { - "0": ["test.html", "nested"], + path: ["test.html", "nested"], }, path: "/test.html/nested", }, }, ], }, - - /** - * Longer prefix. - */ { - path: "/:foo{/test/:bar}?", + path: "/entity/:id/*path", tests: [ { - input: "/route", - expected: { path: "/route", params: { foo: "route" } }, + input: "/entity/foo", + expected: false, }, { - input: "/route/test/again", + input: "/entity/foo/path", expected: { - path: "/route/test/again", - params: { foo: "route", bar: "again" }, + path: "/entity/foo/path", + params: { id: "foo", path: ["path"] }, + }, + }, + ], + }, + { + path: "/*foo/:bar/*baz", + tests: [ + { + input: "/x/y/z", + expected: { + path: "/x/y/z", + params: { foo: ["x"], bar: "y", baz: ["z"] }, + }, + }, + { + input: "/1/2/3/4/5", + expected: { + path: "/1/2/3/4/5", + params: { foo: ["1", "2", "3"], bar: "4", baz: ["5"] }, }, }, ], }, /** - * Prefix and suffix as separator. + * Longer prefix. */ { - path: "/{<:foo>}+", + path: "/:foo{/test/:bar}", tests: [ { - input: "/", - expected: { path: "/", params: { foo: ["test"] } }, + input: "/route", + expected: { path: "/route", params: { foo: "route" } }, }, { - input: "/", + input: "/route/test/again", expected: { - path: "/", - params: { foo: ["test", "again"] }, + path: "/route/test/again", + params: { foo: "route", bar: "again" }, }, }, ], @@ -2046,7 +1313,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Backtracking tests. */ { - path: "{:foo/}?{:bar.}?", + path: "{:foo/}{:bar.}", tests: [ { input: "", @@ -2066,7 +1333,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/abc{abc:foo}?", + path: "/abc{abc:foo}", tests: [ { input: "/abc", @@ -2094,19 +1361,28 @@ export const MATCH_TESTS: MatchTestSet[] = [ ], }, { - path: "/:foo{abc:bar}?", + path: "/:foo{abc:bar}", tests: [ { input: "/abc", - expected: false, + expected: { + params: { foo: "abc" }, + path: "/abc", + }, }, { input: "/abcabc", - expected: false, + expected: { + params: { foo: "abcabc" }, + path: "/abcabc", + }, }, { input: "/abcabc123", - expected: false, + expected: { + params: { foo: "abc", bar: "123" }, + path: "/abcabc123", + }, }, { input: "/acb", @@ -2116,10 +1392,17 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, }, { - input: "/acbabc123", + input: "/123", + expected: { + path: "/123", + params: { foo: "123" }, + }, + }, + { + input: "/123abcabc", expected: { - path: "/acbabc123", - params: { foo: "acb", bar: "123" }, + path: "/123abcabc", + params: { foo: "123abcabc" }, }, }, ], @@ -2137,20 +1420,14 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, { input: "/abcabc123", - expected: false, - }, - ], - }, - { - path: "/:foo(.*){.:ext}?", - tests: [ - { - input: "/abc", - expected: { path: "/abc", params: { foo: "abc" } }, + expected: { + path: "/abcabc123", + params: { foo: "abc", bar: "123" }, + }, }, { - input: "/abc.txt", - expected: { path: "/abc.txt", params: { foo: "abc.txt" } }, + input: "/123abcabc", + expected: false, }, ], }, @@ -2186,6 +1463,22 @@ export const MATCH_TESTS: MatchTestSet[] = [ }, ], }, + { + path: "/:foo{|:bar|}", + tests: [ + { + input: "/hello|world|", + expected: { + path: "/hello|world|", + params: { foo: "hello", bar: "world" }, + }, + }, + { + input: "/hello||", + expected: { path: "/hello||", params: { foo: "hello||" } }, + }, + ], + }, { path: ":foo\\@:bar", tests: [ @@ -2204,7 +1497,7 @@ export const MATCH_TESTS: MatchTestSet[] = [ * Multi character delimiters. */ { - path: "%25:foo{%25:bar}?", + path: "%25:foo{%25:bar}", options: { delimiter: "%25", }, diff --git a/src/index.bench.ts b/src/index.bench.ts new file mode 100644 index 0000000..9d39219 --- /dev/null +++ b/src/index.bench.ts @@ -0,0 +1,42 @@ +import { bench } from "vitest"; +import { match } from "./index.js"; + +const PATHS: string[] = [ + "/xyz", + "/user", + "/user/123", + "/" + "a".repeat(32_000), + "/-" + "-a".repeat(8_000) + "/-", + "/||||\x00|" + "||".repeat(27387) + "|\x00".repeat(27387) + "/||/", +]; + +const STATIC_PATH_MATCH = match("/user"); +const SIMPLE_PATH_MATCH = match("/user/:id"); +const MULTI_SEGMENT_MATCH = match("/:x/:y"); +const MULTI_PATTERN_MATCH = match("/:x-:y"); +const TRICKY_PATTERN_MATCH = match("/:foo|:bar|"); +const ASTERISK_MATCH = match("/*foo"); + +bench("static path", () => { + for (const path of PATHS) STATIC_PATH_MATCH(path); +}); + +bench("simple path", () => { + for (const path of PATHS) SIMPLE_PATH_MATCH(path); +}); + +bench("multi segment", () => { + for (const path of PATHS) MULTI_SEGMENT_MATCH(path); +}); + +bench("multi pattern", () => { + for (const path of PATHS) MULTI_PATTERN_MATCH(path); +}); + +bench("tricky pattern", () => { + for (const path of PATHS) TRICKY_PATTERN_MATCH(path); +}); + +bench("asterisk", () => { + for (const path of PATHS) ASTERISK_MATCH(path); +}); diff --git a/src/index.spec.ts b/src/index.spec.ts index ef019c9..c6da631 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -6,89 +6,86 @@ import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; * Dynamically generate the entire test suite. */ describe("path-to-regexp", () => { - describe("arguments", () => { - it("should throw on non-capturing pattern", () => { - expect(() => match("/:foo(?:\\d+(\\.\\d+)?)")).toThrow( + describe("parse errors", () => { + it("should throw on unbalanced group", () => { + expect(() => parse("/{:foo,")).toThrow( new TypeError( - 'Pattern cannot start with "?" at 6: https://git.new/pathToRegexpError', + "Unexpected END at 7, expected }: https://git.new/pathToRegexpError", ), ); }); - - it("should throw on nested capturing group", () => { - expect(() => match("/:foo(\\d+(\\.\\d+)?)")).toThrow( + it("should throw on nested unbalanced group", () => { + expect(() => parse("/{:foo/{x,y}")).toThrow( new TypeError( - "Capturing groups are not allowed at 9: https://git.new/pathToRegexpError", + "Unexpected END at 12, expected }: https://git.new/pathToRegexpError", ), ); }); - it("should throw on unbalanced pattern", () => { - expect(() => match("/:foo(abc")).toThrow( + it("should throw on missing param name", () => { + expect(() => parse("/:/")).toThrow( new TypeError( - "Unbalanced pattern at 5: https://git.new/pathToRegexpError", + "Missing parameter name at 2: https://git.new/pathToRegexpError", ), ); }); - it("should throw on unmatched )", function () { - expect(() => match("/:fooab)c")).toThrow( - new TypeError("Unmatched ) at 7: https://git.new/pathToRegexpError"), - ); - }); - - it("should throw on unmatched ) after other patterns", function () { - expect(() => match("/:test(\\w+)/:foo(\\d+))")).toThrow( - new TypeError("Unmatched ) at 21: https://git.new/pathToRegexpError"), - ); - }); - - it("should throw on missing pattern", () => { - expect(() => match("/:foo()")).toThrow( + it("should throw on missing wildcard name", () => { + expect(() => parse("/*/")).toThrow( new TypeError( - "Missing pattern at 5: https://git.new/pathToRegexpError", + "Missing parameter name at 2: https://git.new/pathToRegexpError", ), ); }); - it("should throw on missing name", () => { - expect(() => match("/:(test)")).toThrow( + it("should throw on unterminated quote", () => { + expect(() => parse('/:"foo')).toThrow( new TypeError( - "Missing parameter name at 2: https://git.new/pathToRegexpError", + "Unterminated quote at 2: https://git.new/pathToRegexpError", ), ); }); + }); - it("should throw on nested groups", () => { - expect(() => match("/{a{b:foo}}")).toThrow( - new TypeError( - "Unexpected { at 3, expected }: https://git.new/pathToRegexpError", - ), - ); + describe("compile errors", () => { + it("should throw when a param is missing", () => { + const toPath = compile("/a/:b/c"); + + expect(() => { + toPath(); + }).toThrow(new TypeError("Missing parameters: b")); }); - it("should throw on repeat parameters without a separator", () => { - expect(() => match("{:x}*")).toThrow( - new TypeError( - `Missing separator for "x": https://git.new/pathToRegexpError`, - ), - ); + it("should throw when expecting a repeated value", () => { + const toPath = compile("/*foo"); + + expect(() => { + toPath({ foo: [] }); + }).toThrow(new TypeError('Expected "foo" to be a non-empty array')); }); - it("should throw on unterminated quote", () => { - expect(() => match('/:"foo')).toThrow( - new TypeError( - "Unterminated quote at 2: https://git.new/pathToRegexpError", - ), - ); + it("should throw when param gets an array", () => { + const toPath = compile("/:foo"); + + expect(() => { + toPath({ foo: [] }); + }).toThrow(new TypeError('Expected "foo" to be a string')); }); - it("should throw on invalid *", () => { - expect(() => match("/:foo*")).toThrow( - new TypeError( - "Unexpected * at 5, you probably want `/*` or `{/:foo}*`: https://git.new/pathToRegexpError", - ), - ); + it("should throw when a wildcard is not an array", () => { + const toPath = compile("/*foo"); + + expect(() => { + toPath({ foo: "a" }); + }).toThrow(new TypeError('Expected "foo" to be a non-empty array')); + }); + + it("should throw when a wildcard array value is not a string", () => { + const toPath = compile("/*foo"); + + expect(() => { + toPath({ foo: [1, "a"] as any }); + }).toThrow(new TypeError('Expected "foo/0" to be a string')); }); }); @@ -126,75 +123,4 @@ describe("path-to-regexp", () => { }); }, ); - - describe("compile errors", () => { - it("should throw when a required param is undefined", () => { - const toPath = compile("/a/:b/c"); - - expect(() => { - toPath(); - }).toThrow(new TypeError('Expected "b" to be a string')); - }); - - it("should throw when it does not match the pattern", () => { - const toPath = compile("/:foo(\\d+)"); - - expect(() => { - toPath({ foo: "abc" }); - }).toThrow(new TypeError('Invalid value for "foo": "abc"')); - }); - - it("should throw when expecting a repeated value", () => { - const toPath = compile("{/:foo}+"); - - expect(() => { - toPath({ foo: [] }); - }).toThrow(new TypeError('Invalid value for "foo": ""')); - }); - - it("should throw when not expecting a repeated value", () => { - const toPath = compile("/:foo"); - - expect(() => { - toPath({ foo: [] }); - }).toThrow(new TypeError('Expected "foo" to be a string')); - }); - - it("should throw when a repeated param is not an array", () => { - const toPath = compile("{/:foo}+"); - - expect(() => { - toPath({ foo: "a" }); - }).toThrow(new TypeError('Expected "foo" to be an array')); - }); - - it("should throw when an array value is not a string", () => { - const toPath = compile("{/:foo}+"); - - expect(() => { - toPath({ foo: [1, "a"] as any }); - }).toThrow(new TypeError('Expected "foo/0" to be a string')); - }); - - it("should throw when repeated value does not match", () => { - const toPath = compile("{/:foo(\\d+)}+"); - - expect(() => { - toPath({ foo: ["1", "2", "3", "a"] }); - }).toThrow(new TypeError('Invalid value for "foo": "/1/2/3/a"')); - }); - }); }); - -/** - * Execute a regular expression and return a flat array for comparison. - * - * @param {RegExp} re - * @param {String} str - * @return {Array} - */ -function exec(re: RegExp, str: string) { - const match = re.exec(str); - - return match && Array.prototype.slice.call(match); -} diff --git a/src/index.ts b/src/index.ts index 2692df0..a63e365 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,24 +15,13 @@ export type Encode = (value: string) => string; export type Decode = (value: string) => string; export interface ParseOptions { - /** - * The default delimiter for segments. (default: `'/'`) - */ - delimiter?: string; /** * A function for encoding input strings. */ encodePath?: Encode; } -export interface PathOptions { - /** - * Regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; -} - -export interface MatchOptions extends PathOptions { +export interface MatchOptions { /** * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) */ @@ -41,35 +30,47 @@ export interface MatchOptions extends PathOptions { * Matches the path completely without trailing characters. (default: `true`) */ end?: boolean; -} - -export interface CompileOptions extends PathOptions { /** - * Verifies the function is producing a valid path. (default: `true`) + * Allows optional trailing delimiter to match. (default: `true`) + */ + trailing?: boolean; + /** + * Match will be case sensitive. (default: `false`) + */ + sensitive?: boolean; + /** + * The default delimiter for segments. (default: `'/'`) */ - validate?: boolean; + delimiter?: string; +} + +export interface CompileOptions { /** * Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) */ encode?: Encode | false; + /** + * The default delimiter for segments. (default: `'/'`) + */ + delimiter?: string; } type TokenType = | "{" | "}" - | ";" - | "*" - | "+" - | "?" - | "NAME" - | "PATTERN" + | "WILDCARD" + | "PARAM" | "CHAR" | "ESCAPED" | "END" - // Reserved for use. - | "!" - | "@" - | ","; + // Reserved for use or ambiguous due to past use. + | "(" + | ")" + | "[" + | "]" + | "+" + | "?" + | "!"; /** * Tokenizer results. @@ -81,153 +82,120 @@ interface LexToken { } const SIMPLE_TOKENS: Record = { - "!": "!", - "@": "@", - ";": ";", - ",": ",", - "*": "*", - "+": "+", - "?": "?", + // Groups. "{": "{", "}": "}", + // Reserved. + "(": "(", + ")": ")", + "[": "[", + "]": "]", + "+": "+", + "?": "?", + "!": "!", }; +/** + * Escape a regular expression string. + */ +function escape(str: string) { + return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); +} + +/** + * Get the flags for a regexp from the options. + */ +function toFlags(options: { sensitive?: boolean }) { + return options.sensitive ? "s" : "is"; +} + /** * Tokenize input string. */ -function lexer(str: string) { +function* lexer(str: string): Generator { const chars = [...str]; - const tokens: LexToken[] = []; let i = 0; - while (i < chars.length) { - const value = chars[i]; - const type = SIMPLE_TOKENS[value]; - - if (type) { - tokens.push({ type, index: i++, value }); - continue; - } + function name() { + let value = ""; - if (value === "\\") { - tokens.push({ type: "ESCAPED", index: i++, value: chars[i++] }); - continue; - } - - if (value === ":") { - let name = ""; - - if (ID_START.test(chars[++i])) { - name += chars[i]; - while (ID_CONTINUE.test(chars[++i])) { - name += chars[i]; - } - } else if (chars[i] === '"') { - let pos = i; - - while (i < chars.length) { - if (chars[++i] === '"') { - i++; - pos = 0; - break; - } - - if (chars[i] === "\\") { - name += chars[++i]; - } else { - name += chars[i]; - } - } - - if (pos) { - throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); - } - } - - if (!name) { - throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); - } - - tokens.push({ type: "NAME", index: i, value: name }); - continue; - } - - if (value === "(") { - const pos = i++; - let count = 1; - let pattern = ""; - - if (chars[i] === "?") { - throw new TypeError( - `Pattern cannot start with "?" at ${i}: ${DEBUG_URL}`, - ); + if (ID_START.test(chars[++i])) { + value += chars[i]; + while (ID_CONTINUE.test(chars[++i])) { + value += chars[i]; } + } else if (chars[i] === '"') { + let pos = i; while (i < chars.length) { - if (chars[i] === "\\") { - pattern += chars[i++] + chars[i++]; - continue; + if (chars[++i] === '"') { + i++; + pos = 0; + break; } - if (chars[i] === ")") { - count--; - if (count === 0) { - i++; - break; - } - } else if (chars[i] === "(") { - count++; - if (chars[i + 1] !== "?") { - throw new TypeError( - `Capturing groups are not allowed at ${i}: ${DEBUG_URL}`, - ); - } + if (chars[i] === "\\") { + value += chars[++i]; + } else { + value += chars[i]; } - - pattern += chars[i++]; } - if (count) { - throw new TypeError(`Unbalanced pattern at ${pos}: ${DEBUG_URL}`); + if (pos) { + throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); } - - if (!pattern) { - throw new TypeError(`Missing pattern at ${pos}: ${DEBUG_URL}`); - } - - tokens.push({ type: "PATTERN", index: i, value: pattern }); - continue; } - if (value === ")") { - throw new TypeError(`Unmatched ) at ${i}: ${DEBUG_URL}`); + if (!value) { + throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); } - tokens.push({ type: "CHAR", index: i, value: chars[i++] }); + return value; } - tokens.push({ type: "END", index: i, value: "" }); + while (i < chars.length) { + const value = chars[i]; + const type = SIMPLE_TOKENS[value]; - return new Iter(tokens); + if (type) { + yield { type, index: i++, value }; + } else if (value === "\\") { + yield { type: "ESCAPED", index: i++, value: chars[i++] }; + } else if (value === ":") { + const value = name(); + yield { type: "PARAM", index: i, value }; + } else if (value === "*") { + const value = name(); + yield { type: "WILDCARD", index: i, value }; + } else { + yield { type: "CHAR", index: i, value: chars[i++] }; + } + } + + return { type: "END", index: i, value: "" }; } class Iter { - index = 0; + #peek?: LexToken; - constructor(private tokens: LexToken[]) {} + constructor(private tokens: Generator) {} peek(): LexToken { - return this.tokens[this.index]; + if (!this.#peek) { + const next = this.tokens.next(); + this.#peek = next.value; + } + return this.#peek; } - tryConsume(type: LexToken["type"]): string | undefined { + tryConsume(type: TokenType): string | undefined { const token = this.peek(); if (token.type !== type) return; - this.index++; + this.#peek = undefined; // Reset after consumed. return token.value; } - consume(type: LexToken["type"]): string { + consume(type: TokenType): string { const value = this.tryConsume(type); if (value !== undefined) return value; const { type: nextType, index } = this.peek(); @@ -244,223 +212,216 @@ class Iter { } return result; } +} - modifier(): string | undefined { - return this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+"); - } +/** + * Plain text. + */ +export interface Text { + type: "text"; + value: string; } /** - * Tokenized path instance. Can we passed around instead of string. + * A parameter designed to match arbitrary text within a segment. + */ +export interface Parameter { + type: "param"; + name: string; +} + +/** + * A wildcard parameter designed to match multiple segments. + */ +export interface Wildcard { + type: "wildcard"; + name: string; +} + +/** + * A set of possible tokens to expand when matching. + */ +export interface Group { + type: "group"; + tokens: Token[]; +} + +/** + * A sequence of path match characters. + */ +export type Token = Text | Parameter | Wildcard | Group; + +/** + * Tokenized path instance. */ export class TokenData { - constructor( - public readonly tokens: Token[], - public readonly delimiter: string, - ) {} + constructor(public readonly tokens: Token[]) {} } /** * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): TokenData { - const { encodePath = NOOP_VALUE, delimiter = encodePath(DEFAULT_DELIMITER) } = - options; - const tokens: Token[] = []; - const it = lexer(str); - let key = 0; - - do { - const path = it.text(); - if (path) tokens.push(encodePath(path)); - - const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN"); - - if (name || pattern) { - tokens.push({ - name: name || String(key++), - pattern, - }); - - const next = it.peek(); - if (next.type === "*") { - throw new TypeError( - `Unexpected * at ${next.index}, you probably want \`/*\` or \`{/:foo}*\`: ${DEBUG_URL}`, - ); + const { encodePath = NOOP_VALUE } = options; + const it = new Iter(lexer(str)); + + function consume(endType: TokenType): Token[] { + const tokens: Token[] = []; + + while (true) { + const path = it.text(); + if (path) tokens.push({ type: "text", value: encodePath(path) }); + + const param = it.tryConsume("PARAM"); + if (param) { + tokens.push({ + type: "param", + name: param, + }); + continue; } - continue; - } + const wildcard = it.tryConsume("WILDCARD"); + if (wildcard) { + tokens.push({ + type: "wildcard", + name: wildcard, + }); + continue; + } - const asterisk = it.tryConsume("*"); - if (asterisk) { - tokens.push({ - name: String(key++), - pattern: `${negate(delimiter)}*`, - modifier: "*", - separator: delimiter, - }); - continue; - } + const open = it.tryConsume("{"); + if (open) { + tokens.push({ + type: "group", + tokens: consume("}"), + }); + continue; + } - const open = it.tryConsume("{"); - if (open) { - const prefix = it.text(); - const name = it.tryConsume("NAME"); - const pattern = it.tryConsume("PATTERN"); - const suffix = it.text(); - const separator = it.tryConsume(";") && it.text(); - - it.consume("}"); - - const modifier = it.modifier(); - - tokens.push({ - name: name || (pattern ? String(key++) : ""), - prefix: encodePath(prefix), - suffix: encodePath(suffix), - pattern, - modifier, - separator, - }); - continue; + it.consume(endType); + return tokens; } + } - it.consume("END"); - break; - } while (true); + const tokens = consume("END"); + return new TokenData(tokens); +} + +/** + * Transform tokens into a path building function. + */ +function $compile

( + data: TokenData, + options: CompileOptions, +): PathFunction

{ + const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = + options; + const fn = tokensToFunction(data.tokens, delimiter, encode); - return new TokenData(tokens, delimiter); + return function path(data: P = {} as P) { + const [path, ...missing] = fn(data); + if (missing.length) { + throw new TypeError(`Missing parameters: ${missing.join(", ")}`); + } + return path; + }; } /** * Compile a string to a template function for the path. */ export function compile

( - path: string, + path: Path, options: CompileOptions & ParseOptions = {}, ) { - return $compile

(parse(path, options), options); + return $compile

( + path instanceof TokenData ? path : parse(path, options), + options, + ); } export type ParamData = Partial>; export type PathFunction

= (data?: P) => string; -/** - * Check if a key repeats. - */ -export function isRepeat(key: Key) { - return key.modifier === "+" || key.modifier === "*"; -} +function tokensToFunction( + tokens: Token[], + delimiter: string, + encode: Encode | false, +) { + const encoders = tokens.map((token) => + tokenToFunction(token, delimiter, encode), + ); -/** - * Check if a key is optional. - */ -export function isOptional(key: Key) { - return key.modifier === "?" || key.modifier === "*"; + return (data: ParamData) => { + const result: string[] = [""]; + + for (const encoder of encoders) { + const [value, ...extras] = encoder(data); + result[0] += value; + result.push(...extras); + } + + return result; + }; } /** * Convert a single token into a path building function. */ -function keyToFunction( - key: Key, +function tokenToFunction( + token: Token, + delimiter: string, encode: Encode | false, -): (data: ParamData) => string { - const encodeValue = encode || NOOP_VALUE; - const { prefix = "", suffix = "", separator = suffix + prefix } = key; - - if (encode && isRepeat(key)) { - const stringify = (value: string, index: number) => { - if (typeof value !== "string") { - throw new TypeError(`Expected "${key.name}/${index}" to be a string`); - } - return encodeValue(value); - }; - - const compile = (value: unknown) => { - if (!Array.isArray(value)) { - throw new TypeError(`Expected "${key.name}" to be an array`); - } +): (data: ParamData) => string[] { + if (token.type === "text") return () => [token.value]; - if (value.length === 0) return ""; + if (token.type === "group") { + const fn = tokensToFunction(token.tokens, delimiter, encode); - return prefix + value.map(stringify).join(separator) + suffix; + return (data) => { + const [value, ...missing] = fn(data); + if (!missing.length) return [value]; + return [""]; }; + } - if (isOptional(key)) { - return (data): string => { - const value = data[key.name]; - if (value == null) return ""; - return value.length ? compile(value) : ""; - }; - } + const encodeValue = encode || NOOP_VALUE; - return (data): string => { - const value = data[key.name]; - return compile(value); - }; - } + if (token.type === "wildcard" && encode !== false) { + return (data) => { + const value = data[token.name]; + if (value == null) return ["", token.name]; - const stringify = (value: unknown) => { - if (typeof value !== "string") { - throw new TypeError(`Expected "${key.name}" to be a string`); - } - return prefix + encodeValue(value) + suffix; - }; + if (!Array.isArray(value) || value.length === 0) { + throw new TypeError(`Expected "${token.name}" to be a non-empty array`); + } - if (isOptional(key)) { - return (data): string => { - const value = data[key.name]; - if (value == null) return ""; - return stringify(value); + return [ + value + .map((value, index) => { + if (typeof value !== "string") { + throw new TypeError( + `Expected "${token.name}/${index}" to be a string`, + ); + } + + return encodeValue(value); + }) + .join(delimiter), + ]; }; } - return (data): string => { - const value = data[key.name]; - return stringify(value); - }; -} + return (data) => { + const value = data[token.name]; + if (value == null) return ["", token.name]; -/** - * Transform tokens into a path building function. - */ -export function $compile

( - data: TokenData, - options: CompileOptions, -): PathFunction

{ - const { encode = encodeURIComponent, validate = true } = options; - const flags = toFlags(options); - const sources = toRegExpSource(data, []); - - // Compile all the tokens into regexps. - const encoders: Array<(data: ParamData) => string> = data.tokens.map( - (token, index) => { - if (typeof token === "string") return () => token; - - const fn = keyToFunction(token, encode); - if (!validate) return fn; - - const validRe = new RegExp(`^${sources[index]}$`, flags); - - return (data) => { - const value = fn(data); - if (!validRe.test(value)) { - throw new TypeError( - `Invalid value for "${token.name}": ${JSON.stringify(value)}`, - ); - } - return value; - }; - }, - ); + if (typeof value !== "string") { + throw new TypeError(`Expected "${token.name}" to be a string`); + } - return function path(data: Record = {}) { - let path = ""; - for (const encoder of encoders) path += encoder(data); - return path; + return [encodeValue(value)]; }; } @@ -485,34 +446,38 @@ export type MatchFunction

= (path: string) => Match

; /** * Create path match function from `path-to-regexp` spec. */ -export function $match

( - data: TokenData, +function $match

( + data: TokenData[], options: MatchOptions = {}, ): MatchFunction

{ - const { decode = decodeURIComponent, end = true } = options; - const { delimiter } = data; - const keys: Key[] = []; + const { + decode = decodeURIComponent, + delimiter = DEFAULT_DELIMITER, + end = true, + trailing = true, + } = options; const flags = toFlags(options); - const sources = toRegExpSource(data, keys); - const re = new RegExp( - `^${sources.join("")}(?=${escape(delimiter)}|$)`, - flags, - ); - - const decoders = keys.map((key) => { - if (!decode) return NOOP_VALUE; + const sources: string[] = []; + const keys: Array = []; - if (isRepeat(key)) { - const { prefix = "", suffix = "", separator = suffix + prefix } = key; - return (value: string) => value.split(separator).map(decode); + for (const { tokens } of data) { + for (const seq of flatten(tokens, 0, [])) { + const regexp = sequenceToRegExp(seq, delimiter, keys); + sources.push(regexp); } + } - return decode; - }); + let pattern = `^(?:${sources.join("|")})`; + if (trailing) pattern += `(?:${escape(delimiter)}$)?`; + pattern += end ? "$" : `(?=${escape(delimiter)}|$)`; - const isValid = end - ? (a: string, b: string) => a.length === b.length - : () => true; + const re = new RegExp(pattern, flags); + + const decoders = keys.map((key) => { + if (decode === false) return NOOP_VALUE; + if (key.type === "param") return decode; + return (value: string) => value.split(delimiter).map(decode); + }); return Object.assign( function match(input: string) { @@ -520,7 +485,6 @@ export function $match

( if (!m) return false; const { 0: path } = m; - if (!isValid(input, path)) return false; const params = Object.create(null); for (let i = 1; i < m.length; i++) { @@ -537,132 +501,97 @@ export function $match

( ); } +export type Path = string | TokenData; + export function match

( - path: string, + path: Path | Path[], options: MatchOptions & ParseOptions = {}, ): MatchFunction

{ - return $match(parse(path, options), options); -} + const paths = Array.isArray(path) ? path : [path]; + const items = paths.map((path) => + path instanceof TokenData ? path : parse(path, options), + ); -/** - * Escape a regular expression string. - */ -function escape(str: string) { - return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); + return $match(items, options); } /** - * Get the flags for a regexp from the options. + * Flattened token set. */ -function toFlags(options: { sensitive?: boolean }) { - return options.sensitive ? "s" : "is"; -} +type Flattened = Text | Parameter | Wildcard; /** - * A key is a capture group in the regex. + * Generate a flat list of sequence tokens from the given tokens. */ -export interface Key { - name: string; - prefix?: string; - suffix?: string; - pattern?: string; - modifier?: string; - separator?: string; -} +function* flatten( + tokens: Token[], + index: number, + init: Flattened[], +): Generator { + if (index === tokens.length) { + return yield init; + } -/** - * A token is a string (nothing special) or key metadata (capture group). - */ -export type Token = string | Key; + const token = tokens[index]; + + if (token.type === "group") { + const fork = init.slice(); + for (const seq of flatten(token.tokens, 0, fork)) { + yield* flatten(tokens, index + 1, seq); + } + } else { + init.push(token); + } + + yield* flatten(tokens, index + 1, init); +} /** - * Convert a token into a regexp string (re-used for path validation). + * Transform a flat sequence of tokens into a regular expression. */ -function toRegExpSource(data: TokenData, keys: Key[]): string[] { - const sources = Array(data.tokens.length); +function sequenceToRegExp( + tokens: Flattened[], + delimiter: string, + keys: Array, +): string { + let result = ""; let backtrack = ""; + let isSafeSegmentParam = true; - let i = data.tokens.length; + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; - while (i--) { - const token = data.tokens[i]; - - if (typeof token === "string") { - backtrack = token; - sources[i] = escape(token); + if (token.type === "text") { + result += escape(token.value); + backtrack = token.value; + isSafeSegmentParam ||= token.value.includes(delimiter); continue; } - const { - prefix = "", - suffix = "", - separator = suffix + prefix, - modifier = "", - } = token; - - const pre = escape(prefix); - const post = escape(suffix); - - if (token.name) { - backtrack = suffix || backtrack; - keys.unshift(token); - - if (isRepeat(token)) { - if (!separator) { - throw new TypeError( - `Missing separator for "${token.name}": ${DEBUG_URL}`, - ); - } + if (token.type === "param" || token.type === "wildcard") { + if (!isSafeSegmentParam && !backtrack) { + throw new TypeError(`Missing text after "${token.name}": ${DEBUG_URL}`); + } - const mod = modifier === "*" ? "?" : ""; - const sep = escape(separator); - const pattern = - token.pattern || `${negate(data.delimiter, separator, backtrack)}+`; - - sources[i] = wrap( - pre, - `(?:${pattern})(?:${sep}(?:${pattern}))*`, - post, - mod, - ); + if (token.type === "param") { + result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`; } else { - sources[i] = wrap( - pre, - token.pattern || `${negate(data.delimiter, backtrack)}+`, - post, - modifier, - ); + result += `(.+)`; } - backtrack = prefix; - } else { - sources[i] = `(?:${pre}${post})${modifier}`; - backtrack = `${prefix}${suffix}`; + keys.push(token); + backtrack = ""; + isSafeSegmentParam = false; + continue; } } - return sources; + return result; } -function negate(...args: string[]) { - const values = args.sort().filter((value, index, array) => { - for (let i = 0; i < index; i++) { - const v = array[i]; - if (v.length && value.startsWith(v)) return false; - } - return value.length > 0; - }); - +function negate(delimiter: string, backtrack: string) { + const values = [delimiter, backtrack].filter(Boolean); const isSimple = values.every((value) => value.length === 1); if (isSimple) return `[^${escape(values.join(""))}]`; - return `(?:(?!${values.map(escape).join("|")}).)`; } - -function wrap(pre: string, pattern: string, post: string, modifier: string) { - if (pre || post) { - return `(?:${pre}(${pattern})${post})${modifier}`; - } - - return `(${pattern})${modifier}`; -} diff --git a/tsconfig.build.json b/tsconfig.build.json index d783ab3..3db8e88 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": [] }, - "exclude": ["src/**/*.spec.ts"] + "exclude": ["src/**/*.spec.ts", "src/**/*.bench.ts"] } From ed1095e0fa78a692e7f3d489e383e7bb1f9d2cc4 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Sun, 1 Sep 2024 15:14:38 -0700 Subject: [PATCH 12/12] 8.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 172848d..a582d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "path-to-regexp", - "version": "7.1.0", + "version": "8.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "path-to-regexp", - "version": "7.1.0", + "version": "8.0.0", "license": "MIT", "devDependencies": { "@borderless/ts-scripts": "^0.15.0", diff --git a/package.json b/package.json index a57212a..f6806a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "path-to-regexp", - "version": "7.1.0", + "version": "8.0.0", "description": "Express style path to RegExp utility", "keywords": [ "express",