diff --git a/README.md b/README.md index ebaf59a..2f1265c 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ findRoute(router, "GET", "/"); > [!IMPORTANT] > Method should **always be UPPERCASE**. +> [!TIP] +> If you need to register a pattern containing literal `:` or `*`, you can escape them with `\\`. For example, `/static\\:path/\\*\\*` matches only the static `/static:path/**` route. + ## Compiler diff --git a/src/operations/add.ts b/src/operations/add.ts index a2d280c..a905e22 100644 --- a/src/operations/add.ts +++ b/src/operations/add.ts @@ -16,6 +16,8 @@ export function addRoute( path = `/${path}`; } + path = path.replace(/\\:/g, "%3A"); + const segments = splitPath(path); let node = ctx.root; @@ -26,7 +28,7 @@ export function addRoute( const paramsRegexp: RegExp[] = []; for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; + let segment = segments[i]; // Wildcard if (segment.startsWith("**")) { @@ -64,6 +66,11 @@ export function addRoute( } // Static + if (segment === "\\*") { + segment = segments[i] = "*"; + } else if (segment === "\\*\\*") { + segment = segments[i] = "**"; + } const child = node.static?.[segment]; if (child) { node = child; @@ -91,7 +98,7 @@ export function addRoute( // Static if (!hasParams) { - ctx.static[path] = node; + ctx.static["/" + segments.join("/")] = node; } } diff --git a/test/.snapshot/compiled-aot.mjs b/test/.snapshot/compiled-aot.mjs index b17fb74..a370476 100644 --- a/test/.snapshot/compiled-aot.mjs +++ b/test/.snapshot/compiled-aot.mjs @@ -5,13 +5,14 @@ const findRoute = /* @__PURE__ */ (() => { $3 = { path: "/test/foo/baz" }, $4 = { path: "/test/fooo" }, $5 = { path: "/another/path" }, - $6 = { path: "/test/foo/*" }, - $7 = { path: "/test/foo/**" }, - $8 = { path: "/test/:id" }, - $9 = { path: "/test/:idY/y" }, - $10 = { path: "/test/:idYZ/y/z" }, - $11 = { path: "/wildcard/**" }, - $12 = { path: "/**" }; + $6 = { path: "/static\\:path/\\*/\\*\\*" }, + $7 = { path: "/test/foo/*" }, + $8 = { path: "/test/foo/**" }, + $9 = { path: "/test/:id" }, + $10 = { path: "/test/:idY/y" }, + $11 = { path: "/test/:idYZ/y/z" }, + $12 = { path: "/wildcard/**" }, + $13 = { path: "/**" }; return (m, p) => { if (p.charCodeAt(p.length - 1) === 47) p = p.slice(0, -1) || "/"; if (p === "/test") { @@ -32,34 +33,37 @@ const findRoute = /* @__PURE__ */ (() => { if (p === "/another/path") { if (m === "GET") return { data: $5 }; } + if (p === "/static%3Apath/*/**") { + if (m === "GET") return { data: $6 }; + } let s = p.split("/"), l = s.length - 1; if (s[1] === "test") { if (s[2] === "foo") { if (l === 3 || l === 2) { - if (m === "GET") return { data: $6, params: { _0: s[3] } }; + if (m === "GET") return { data: $7, params: { _0: s[3] } }; } if (m === "GET") - return { data: $7, params: { _: s.slice(3).join("/") } }; + return { data: $8, params: { _: s.slice(3).join("/") } }; } if (l === 2 || l === 1) { - if (m === "GET") if (l >= 2) return { data: $8, params: { id: s[2] } }; + if (m === "GET") if (l >= 2) return { data: $9, params: { id: s[2] } }; } if (s[3] === "y") { if (l === 3) { - if (m === "GET") return { data: $9, params: { idY: s[2] } }; + if (m === "GET") return { data: $10, params: { idY: s[2] } }; } if (s[4] === "z") { if (l === 4) { - if (m === "GET") return { data: $10, params: { idYZ: s[2] } }; + if (m === "GET") return { data: $11, params: { idYZ: s[2] } }; } } } } if (s[1] === "wildcard") { if (m === "GET") - return { data: $11, params: { _: s.slice(2).join("/") } }; + return { data: $12, params: { _: s.slice(2).join("/") } }; } - if (m === "GET") return { data: $12, params: { _: s.slice(1).join("/") } }; + if (m === "GET") return { data: $13, params: { _: s.slice(1).join("/") } }; }; })(); diff --git a/test/.snapshot/compiled-jit.mjs b/test/.snapshot/compiled-jit.mjs index fa0f574..1d1f59f 100644 --- a/test/.snapshot/compiled-jit.mjs +++ b/test/.snapshot/compiled-jit.mjs @@ -18,31 +18,34 @@ if (p === "/another/path") { if (m === "GET") return { data: $5 }; } + if (p === "/static%3Apath/*/**") { + if (m === "GET") return { data: $6 }; + } let s = p.split("/"), l = s.length - 1; if (s[1] === "test") { if (s[2] === "foo") { if (l === 3 || l === 2) { - if (m === "GET") return { data: $6, params: { _0: s[3] } }; + if (m === "GET") return { data: $7, params: { _0: s[3] } }; } - if (m === "GET") return { data: $7, params: { _: s.slice(3).join("/") } }; + if (m === "GET") return { data: $8, params: { _: s.slice(3).join("/") } }; } if (l === 2 || l === 1) { - if (m === "GET") if (l >= 2) return { data: $8, params: { id: s[2] } }; + if (m === "GET") if (l >= 2) return { data: $9, params: { id: s[2] } }; } if (s[3] === "y") { if (l === 3) { - if (m === "GET") return { data: $9, params: { idY: s[2] } }; + if (m === "GET") return { data: $10, params: { idY: s[2] } }; } if (s[4] === "z") { if (l === 4) { - if (m === "GET") return { data: $10, params: { idYZ: s[2] } }; + if (m === "GET") return { data: $11, params: { idYZ: s[2] } }; } } } } if (s[1] === "wildcard") { - if (m === "GET") return { data: $11, params: { _: s.slice(2).join("/") } }; + if (m === "GET") return { data: $12, params: { _: s.slice(2).join("/") } }; } - if (m === "GET") return { data: $12, params: { _: s.slice(1).join("/") } }; + if (m === "GET") return { data: $13, params: { _: s.slice(1).join("/") } }; }; diff --git a/test/find.test.ts b/test/find.test.ts index 5c656a6..0f2a782 100644 --- a/test/find.test.ts +++ b/test/find.test.ts @@ -18,6 +18,7 @@ describe("route matching", () => { "/test/fooo", "/another/path", "/wildcard/**", + "/static\\:path/\\*/\\*\\*", "/**", ]); @@ -41,6 +42,9 @@ describe("route matching", () => { │ ├── /path ┈> [GET] /another/path ├── /wildcard │ ├── /** ┈> [GET] /wildcard/** + ├── /static%3Apath + │ ├── /* + │ │ ├── /** ┈> [GET] /static\\:path/\\*/\\*\\* ├── /** ┈> [GET] /**" `); }); @@ -134,6 +138,13 @@ describe("route matching", () => { data: { path: "/**" }, params: { _: "any/deep/path" }, }); + // Escaped characters + expect(match("GET", "/static%3Apath/*/**")).toMatchObject({ + data: { path: "/static\\:path/\\*/\\*\\*" }, + }); + expect(match("GET", "/static:path/some/deep/path")).toMatchObject({ + data: { path: "/**" }, // should not match static route + }); }); } @@ -157,7 +168,10 @@ describe("route matching", () => { ├── /another │ ├── /path ┈> [GET] /another/path ├── /wildcard - │ ├── /** ┈> [GET] /wildcard/**" + │ ├── /** ┈> [GET] /wildcard/** + ├── /static%3Apath + │ ├── /* + │ │ ├── /** ┈> [GET] /static\\:path/\\*/\\*\\*" `); expect(findRoute(router, "GET", "/test")).toBeUndefined(); }); diff --git a/test/regexp.test.ts b/test/regexp.test.ts index 3d16940..8f91740 100644 --- a/test/regexp.test.ts +++ b/test/regexp.test.ts @@ -44,6 +44,10 @@ describe("routeToRegExp", () => { regex: /^\/base\/?(?.+)\/?$/, match: [["/base/anything/more", { path: "anything/more" }]], }, + "/static%3Apath/\\*/\\*\\*": { + regex: /^\/static%3Apath\/\*\/\*\*\/?$/, + match: [["/static%3Apath/*/**"]], + }, "/**": { regex: /^\/?(?<_>.*)\/?$/, match: [ diff --git a/test/router.test.ts b/test/router.test.ts index 9f3fc10..4cf1585 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -486,6 +486,7 @@ describe("Router insert", () => { "/api/v1", "/api/v2", "/api/v3", + "/static\\:path/\\*\\*", ]); addRoute(router, "", "/api/v3", { @@ -510,7 +511,9 @@ describe("Router insert", () => { ├── /api │ ├── /v1 ┈> [GET] /api/v1 │ ├── /v2 ┈> [GET] /api/v2 - │ ├── /v3 ┈> [GET] /api/v3, [*] /api/v3(overridden)" + │ ├── /v3 ┈> [GET] /api/v3, [*] /api/v3(overridden) + ├── /static%3Apath + │ ├── /** ┈> [GET] /static\\:path/\\*\\*" `); }); });