diff --git a/package.json b/package.json index 9fe384b..0844a52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@btst/yar", - "version": "1.1.1", + "version": "1.2.0", "packageManager": "pnpm@10.14.0", "description": "Pluggable router for modern react frameworks", "type": "module", diff --git a/src/__tests__/router.test.ts b/src/__tests__/router.test.ts index 0b19f4f..83539e1 100644 --- a/src/__tests__/router.test.ts +++ b/src/__tests__/router.test.ts @@ -994,3 +994,56 @@ describe("Edge cases", () => { } }); }); + +describe("routeKey", () => { + it("should expose the route key on a matched route", () => { + const routes = { + home: createRoute("/", () => ({ PageComponent: MockComponent })), + about: createRoute("/about", () => ({ PageComponent: AnotherComponent })), + }; + + const router = createRouter(routes); + + expect(router.getRoute("/")?.routeKey).toBe("home"); + expect(router.getRoute("/about")?.routeKey).toBe("about"); + }); + + it("should return null for unmatched paths (no routeKey)", () => { + const routes = { + home: createRoute("/", () => ({ PageComponent: MockComponent })), + }; + + const router = createRouter(routes); + expect(router.getRoute("/missing")).toBeNull(); + }); + + it("should set the correct routeKey for parameterised routes", () => { + const routes = { + post: createRoute("/posts/:slug", () => ({ + PageComponent: MockComponent, + })), + user: createRoute("/users/:id", () => ({ + PageComponent: AnotherComponent, + })), + }; + + const router = createRouter(routes); + + expect(router.getRoute("/posts/hello-world")?.routeKey).toBe("post"); + expect(router.getRoute("/users/42")?.routeKey).toBe("user"); + }); + + it("should set the correct routeKey when exact and parameterised routes overlap", () => { + const routes = { + me: createRoute("/users/me", () => ({ PageComponent: MockComponent })), + user: createRoute("/users/:id", () => ({ + PageComponent: AnotherComponent, + })), + }; + + const router = createRouter(routes); + + expect(router.getRoute("/users/me")?.routeKey).toBe("me"); + expect(router.getRoute("/users/123")?.routeKey).toBe("user"); + }); +}); diff --git a/src/router.ts b/src/router.ts index 3ff2eae..264921c 100644 --- a/src/router.ts +++ b/src/router.ts @@ -104,11 +104,13 @@ type ExtractRouteReturn = R extends (...args: any[]) => infer Return ? Return : never; -// The return type for getRoute, combining each handler return with params -// We distribute the intersection over the union to preserve all properties +// The return type for getRoute, combining each handler return with params and routeKey. +// Distributing the mapped type over the union preserves the discriminated union so +// TypeScript can narrow params and other properties based on routeKey. type GetRouteReturn> = { [K in keyof Routes]: ExtractRouteReturn & { params: Record; + routeKey: K; }; }[keyof Routes]; @@ -192,6 +194,12 @@ export const createRouter = < extra, } = responseObj; + // Resolve the route key by finding the entry whose handler is the same + // reference as the one the internal router matched. + const routeKey = Object.entries(routes).find( + ([, v]) => v === handler, + )?.[0] as keyof E | undefined; + return { PageComponent, LoadingComponent, @@ -200,6 +208,7 @@ export const createRouter = < loader, meta, extra, + routeKey, } as GetRouteReturn; }, };