diff --git a/packages/fresh/src/router.ts b/packages/fresh/src/router.ts index c0bbfa9293a..0b1ec442436 100644 --- a/packages/fresh/src/router.ts +++ b/packages/fresh/src/router.ts @@ -1,3 +1,5 @@ +import { sortRoutePaths } from "./fs_routes.ts"; + export type Method = | "HEAD" | "GET" @@ -89,6 +91,20 @@ export class UrlPatternRouter implements Router { }; this.#dynamics.set(pathname, def); this.#dynamicArr.push(def); + this.#dynamicArr.sort((a, b) => { + const aPath = a.pattern.pathname; + const bPath = b.pattern.pathname; + // Convert URLPattern format (:param, :param*) to + // filesystem route format ([param], [...param]) so that + // sortRoutePaths can rank by specificity. + const aFs = aPath + .replace(/:(\w+)\*/g, "[...$1]") + .replace(/:(\w+)/g, "[$1]"); + const bFs = bPath + .replace(/:(\w+)\*/g, "[...$1]") + .replace(/:(\w+)/g, "[$1]"); + return sortRoutePaths(aFs, bFs); + }); } byMethod = def.byMethod; diff --git a/packages/fresh/src/router_test.ts b/packages/fresh/src/router_test.ts index 479f2f8439c..4a679e558d1 100644 --- a/packages/fresh/src/router_test.ts +++ b/packages/fresh/src/router_test.ts @@ -319,3 +319,47 @@ Deno.test("UrlPatternRouter - non-standard method on dynamic route", () => { pattern: "/books/:id", }); }); + +Deno.test("UrlPatternRouter - specific route matches before catch-all regardless of registration order", () => { + const router = new UrlPatternRouter<() => string>(); + const catchAll = () => "catch-all"; + const specific = () => "specific"; + + // Register catch-all first, then specific route + router.add("GET", "/blog/:rest*", catchAll); + router.add("GET", "/blog/:id", specific); + + // Specific route should match /blog/123 + const res = router.match("GET", new URL("/blog/123", "http://localhost")); + expect(res.item).toBe(specific); + expect(res.params).toEqual({ id: "123" }); + + // Catch-all should still match paths the specific route doesn't + const res2 = router.match( + "GET", + new URL("/blog/a/b/c", "http://localhost"), + ); + expect(res2.item).toBe(catchAll); + expect(res2.params).toEqual({ rest: "a/b/c" }); +}); + +Deno.test("UrlPatternRouter - multiple dynamic routes sorted by specificity", () => { + const router = new UrlPatternRouter<() => string>(); + const catchAll = () => "catch-all"; + const byId = () => "by-id"; + const byName = () => "by-name"; + + // Register catch-all first, then specific routes + router.add("GET", "/api/:rest*", catchAll); + router.add("GET", "/api/:name", byName); + router.add("GET", "/api/:id", byId); + + // Registration order was: catchAll, byName, byId + // After sorting, more specific routes should match first. + // /api/:id and /api/:name are equally specific (both dynamic), + // so the sort order between them is stable/deterministic. + const res = router.match("GET", new URL("/api/hello", "http://localhost")); + // Should match one of the specific routes, not the catch-all + expect(res.item).not.toBe(catchAll); + expect(res.methodMatch).toBe(true); +});