diff --git a/.changeset/afraid-snakes-cough.md b/.changeset/afraid-snakes-cough.md new file mode 100644 index 0000000000..84b44fcf75 --- /dev/null +++ b/.changeset/afraid-snakes-cough.md @@ -0,0 +1,7 @@ +--- +"@remix-run/router": patch +--- + +Add `requestContext` support to static handler `query`/`queryRoute` + +- Note that the unstable API of `queryRoute(path, routeId)` has been changed to `queryRoute(path, { routeId, requestContext })` diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index 767e0f2c2a..8d5d58eae8 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -10932,6 +10932,42 @@ describe("a router", () => { expect(childLoaderRequest.url).toBe("http://localhost/child"); }); + it("should support a requestContext passed to loaders and actions", async () => { + let requestContext = { sessionId: "12345" }; + let rootStub = jest.fn(() => "ROOT"); + let childStub = jest.fn(() => "CHILD"); + let actionStub = jest.fn(() => "CHILD ACTION"); + let arg = (s) => s.mock.calls[0][0]; + let { query } = createStaticHandler([ + { + id: "root", + path: "/", + loader: rootStub, + children: [ + { + id: "child", + path: "child", + action: actionStub, + loader: childStub, + }, + ], + }, + ]); + + await query(createRequest("/child"), { requestContext }); + expect(arg(rootStub).context.sessionId).toBe("12345"); + expect(arg(childStub).context.sessionId).toBe("12345"); + + actionStub.mockClear(); + rootStub.mockClear(); + childStub.mockClear(); + + await query(createSubmitRequest("/child"), { requestContext }); + expect(arg(actionStub).context.sessionId).toBe("12345"); + expect(arg(rootStub).context.sessionId).toBe("12345"); + expect(arg(childStub).context.sessionId).toBe("12345"); + }); + describe("statusCode", () => { it("should expose a 200 status code by default", async () => { let { query } = createStaticHandler([ @@ -11254,7 +11290,7 @@ describe("a router", () => { isError ? Promise.reject(data) : Promise.resolve(data), }, ]); - return handler.queryRoute(req, routeId); + return handler.queryRoute(req, { routeId }); } return { @@ -11307,7 +11343,9 @@ describe("a router", () => { data = await queryRoute(createRequest("/parent?index")); expect(data).toBe("PARENT INDEX LOADER"); - data = await queryRoute(createRequest("/parent/child"), "child"); + data = await queryRoute(createRequest("/parent/child"), { + routeId: "child", + }); expect(data).toBe("CHILD LOADER"); }); @@ -11324,19 +11362,27 @@ describe("a router", () => { let data; // Layout route - data = await queryRoute(createRequest("/parent"), "parent"); + data = await queryRoute(createRequest("/parent"), { + routeId: "parent", + }); expect(data).toBe("PARENT LOADER"); // Index route - data = await queryRoute(createRequest("/parent"), "parentIndex"); + data = await queryRoute(createRequest("/parent"), { + routeId: "parentIndex", + }); expect(data).toBe("PARENT INDEX LOADER"); // Parent in nested route - data = await queryRoute(createRequest("/parent/child"), "parent"); + data = await queryRoute(createRequest("/parent/child"), { + routeId: "parent", + }); expect(data).toBe("PARENT LOADER"); // Child in nested route - data = await queryRoute(createRequest("/parent/child"), "child"); + data = await queryRoute(createRequest("/parent/child"), { + routeId: "child", + }); expect(data).toBe("CHILD LOADER"); // Non-undefined falsey values should count @@ -11464,19 +11510,27 @@ describe("a router", () => { let data; // Layout route - data = await queryRoute(createRequest("/base/parent"), "parent"); + data = await queryRoute(createRequest("/base/parent"), { + routeId: "parent", + }); expect(data).toBe("PARENT LOADER"); // Index route - data = await queryRoute(createRequest("/base/parent"), "parentIndex"); + data = await queryRoute(createRequest("/base/parent"), { + routeId: "parentIndex", + }); expect(data).toBe("PARENT INDEX LOADER"); // Parent in nested route - data = await queryRoute(createRequest("/base/parent/child"), "parent"); + data = await queryRoute(createRequest("/base/parent/child"), { + routeId: "parent", + }); expect(data).toBe("PARENT LOADER"); // Child in nested route - data = await queryRoute(createRequest("/base/parent/child"), "child"); + data = await queryRoute(createRequest("/base/parent/child"), { + routeId: "child", + }); expect(data).toBe("CHILD LOADER"); // Non-undefined falsey values should count @@ -11494,19 +11548,27 @@ describe("a router", () => { let data; // Layout route - data = await queryRoute(createSubmitRequest("/parent"), "parent"); + data = await queryRoute(createSubmitRequest("/parent"), { + routeId: "parent", + }); expect(data).toBe("PARENT ACTION"); // Index route - data = await queryRoute(createSubmitRequest("/parent"), "parentIndex"); + data = await queryRoute(createSubmitRequest("/parent"), { + routeId: "parentIndex", + }); expect(data).toBe("PARENT INDEX ACTION"); // Parent in nested route - data = await queryRoute(createSubmitRequest("/parent/child"), "parent"); + data = await queryRoute(createSubmitRequest("/parent/child"), { + routeId: "parent", + }); expect(data).toBe("PARENT ACTION"); // Child in nested route - data = await queryRoute(createSubmitRequest("/parent/child"), "child"); + data = await queryRoute(createSubmitRequest("/parent/child"), { + routeId: "child", + }); expect(data).toBe("CHILD ACTION"); // Non-undefined falsey values should count @@ -11525,19 +11587,19 @@ describe("a router", () => { data = await queryRoute( createSubmitRequest("/parent", { method: "PUT" }), - "parent" + { routeId: "parent" } ); expect(data).toBe("PARENT ACTION"); data = await queryRoute( createSubmitRequest("/parent", { method: "PATCH" }), - "parent" + { routeId: "parent" } ); expect(data).toBe("PARENT ACTION"); data = await queryRoute( createSubmitRequest("/parent", { method: "DELETE" }), - "parent" + { routeId: "parent" } ); expect(data).toBe("PARENT ACTION"); }); @@ -11697,10 +11759,9 @@ describe("a router", () => { ], }, ]); - let response = await queryRoute( - createRequest("/parent/child"), - "child" - ); + let response = await queryRoute(createRequest("/parent/child"), { + routeId: "child", + }); expect(response instanceof Response).toBe(true); expect((response as Response).status).toBe(302); expect((response as Response).headers.get("Location")).toBe("/parent"); @@ -11724,10 +11785,9 @@ describe("a router", () => { ], }, ]); - let response = await queryRoute( - createSubmitRequest("/parent/child"), - "child" - ); + let response = await queryRoute(createSubmitRequest("/parent/child"), { + routeId: "child", + }); expect(response instanceof Response).toBe(true); expect((response as Response).status).toBe(302); expect((response as Response).headers.get("Location")).toBe("/parent"); @@ -11749,7 +11809,9 @@ describe("a router", () => { loader: () => redirect(url), }, ]); - let response = await handler.queryRoute(createRequest("/"), "root"); + let response = await handler.queryRoute(createRequest("/"), { + routeId: "root", + }); expect(response instanceof Response).toBe(true); expect((response as Response).status).toBe(302); expect((response as Response).headers.get("Location")).toBe(url); @@ -11766,7 +11828,7 @@ describe("a router", () => { }, ]); let request = createRequest("/"); - let data = await queryRoute(request, "root"); + let data = await queryRoute(request, { routeId: "root" }); expect(data instanceof Response).toBe(true); expect(await data.json()).toEqual({ key: "value" }); }); @@ -11781,7 +11843,7 @@ describe("a router", () => { }, ]); let request = createSubmitRequest("/"); - let data = await queryRoute(request, "root"); + let data = await queryRoute(request, { routeId: "root" }); expect(data instanceof Response).toBe(true); expect(await data.json()).toEqual({ key: "value" }); }); @@ -11801,7 +11863,7 @@ describe("a router", () => { }); let e; try { - let statePromise = queryRoute(request, "root"); + let statePromise = queryRoute(request, { routeId: "root" }); controller.abort(); // This should resolve even though we never resolved the loader await statePromise; @@ -11826,7 +11888,7 @@ describe("a router", () => { }); let e; try { - let statePromise = queryRoute(request, "root"); + let statePromise = queryRoute(request, { routeId: "root" }); controller.abort(); // This should resolve even though we never resolved the loader await statePromise; @@ -11841,7 +11903,7 @@ describe("a router", () => { let request = createRequest("/", { signal: undefined }); let e; try { - await queryRoute(request, "index"); + await queryRoute(request, { routeId: "index" }); } catch (_e) { e = _e; } @@ -11850,6 +11912,38 @@ describe("a router", () => { ); }); + it("should support a requestContext passed to loaders and actions", async () => { + let requestContext = { sessionId: "12345" }; + let childStub = jest.fn(() => "CHILD"); + let actionStub = jest.fn(() => "CHILD ACTION"); + let arg = (s) => s.mock.calls[0][0]; + let { queryRoute } = createStaticHandler([ + { + path: "/", + children: [ + { + id: "child", + path: "child", + action: actionStub, + loader: childStub, + }, + ], + }, + ]); + + await queryRoute(createRequest("/child"), { + routeId: "child", + requestContext, + }); + expect(arg(childStub).context.sessionId).toBe("12345"); + + await queryRoute(createSubmitRequest("/child"), { + routeId: "child", + requestContext, + }); + expect(arg(actionStub).context.sessionId).toBe("12345"); + }); + describe("Errors with Status Codes", () => { /* eslint-disable jest/no-conditional-expect */ let { queryRoute } = createStaticHandler([ @@ -11887,7 +11981,7 @@ describe("a router", () => { it("should handle not found routeIds with a 403 Response", async () => { try { - await queryRoute(createRequest("/"), "junk"); + await queryRoute(createRequest("/"), { routeId: "junk" }); expect(false).toBe(true); } catch (data) { expect(isRouteErrorResponse(data)).toBe(true); @@ -11899,7 +11993,7 @@ describe("a router", () => { } try { - await queryRoute(createSubmitRequest("/"), "junk"); + await queryRoute(createSubmitRequest("/"), { routeId: "junk" }); expect(false).toBe(true); } catch (data) { expect(isRouteErrorResponse(data)).toBe(true); @@ -11913,7 +12007,7 @@ describe("a router", () => { it("should handle missing loaders with a 400 Response", async () => { try { - await queryRoute(createRequest("/"), "root"); + await queryRoute(createRequest("/"), { routeId: "root" }); expect(false).toBe(true); } catch (data) { expect(isRouteErrorResponse(data)).toBe(true); @@ -11930,7 +12024,7 @@ describe("a router", () => { it("should handle missing actions with a 405 Response", async () => { try { - await queryRoute(createSubmitRequest("/"), "root"); + await queryRoute(createSubmitRequest("/"), { routeId: "root" }); expect(false).toBe(true); } catch (data) { expect(isRouteErrorResponse(data)).toBe(true); @@ -11947,7 +12041,9 @@ describe("a router", () => { it("should handle unsupported methods with a 405 Response", async () => { try { - await queryRoute(createRequest("/", { method: "OPTIONS" }), "root"); + await queryRoute(createRequest("/", { method: "OPTIONS" }), { + routeId: "root", + }); expect(false).toBe(true); } catch (data) { expect(isRouteErrorResponse(data)).toBe(true); diff --git a/packages/router/router.ts b/packages/router/router.ts index 3a1bf9f2e8..2b97dd2e49 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -316,8 +316,14 @@ export interface StaticHandlerContext { */ export interface StaticHandler { dataRoutes: AgnosticDataRouteObject[]; - query(request: Request): Promise; - queryRoute(request: Request, routeId?: string): Promise; + query( + request: Request, + opts?: { requestContext?: unknown } + ): Promise; + queryRoute( + request: Request, + opts?: { routeId?: string; requestContext?: unknown } + ): Promise; } /** @@ -1943,7 +1949,8 @@ export function unstable_createStaticHandler( * return it directly. */ async function query( - request: Request + request: Request, + { requestContext }: { requestContext?: unknown } = {} ): Promise { let url = new URL(request.url); let method = request.method.toLowerCase(); @@ -1987,7 +1994,7 @@ export function unstable_createStaticHandler( }; } - let result = await queryImpl(request, location, matches); + let result = await queryImpl(request, location, matches, requestContext); if (isResponse(result)) { return result; } @@ -2018,7 +2025,13 @@ export function unstable_createStaticHandler( * code. Examples here are 404 and 405 errors that occur prior to reaching * any user-defined loaders. */ - async function queryRoute(request: Request, routeId?: string): Promise { + async function queryRoute( + request: Request, + { + routeId, + requestContext, + }: { requestContext?: unknown; routeId?: string } = {} + ): Promise { let url = new URL(request.url); let method = request.method.toLowerCase(); let location = createLocation("", createPath(url), null, "default"); @@ -2045,7 +2058,13 @@ export function unstable_createStaticHandler( throw getInternalRouterError(404, { pathname: location.pathname }); } - let result = await queryImpl(request, location, matches, match); + let result = await queryImpl( + request, + location, + matches, + requestContext, + match + ); if (isResponse(result)) { return result; } @@ -2068,6 +2087,7 @@ export function unstable_createStaticHandler( request: Request, location: Location, matches: AgnosticDataRouteMatch[], + requestContext: unknown, routeMatch?: AgnosticDataRouteMatch ): Promise | Response> { invariant( @@ -2081,12 +2101,18 @@ export function unstable_createStaticHandler( request, matches, routeMatch || getTargetMatch(matches, location), + requestContext, routeMatch != null ); return result; } - let result = await loadRouteData(request, matches, routeMatch); + let result = await loadRouteData( + request, + matches, + requestContext, + routeMatch + ); return isResponse(result) ? result : { @@ -2117,6 +2143,7 @@ export function unstable_createStaticHandler( request: Request, matches: AgnosticDataRouteMatch[], actionMatch: AgnosticDataRouteMatch, + requestContext: unknown, isRouteRequest: boolean ): Promise | Response> { let result: DataResult; @@ -2142,7 +2169,8 @@ export function unstable_createStaticHandler( matches, basename, true, - isRouteRequest + isRouteRequest, + requestContext ); if (request.signal.aborted) { @@ -2192,9 +2220,15 @@ export function unstable_createStaticHandler( // Store off the pending error - we use it to determine which loaders // to call and will commit it when we complete the navigation let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); - let context = await loadRouteData(request, matches, undefined, { - [boundaryMatch.route.id]: result.error, - }); + let context = await loadRouteData( + request, + matches, + requestContext, + undefined, + { + [boundaryMatch.route.id]: result.error, + } + ); // action status codes take precedence over loader status codes return { @@ -2211,7 +2245,7 @@ export function unstable_createStaticHandler( // Create a GET request for the loaders let loaderRequest = new Request(request.url, { signal: request.signal }); - let context = await loadRouteData(loaderRequest, matches); + let context = await loadRouteData(loaderRequest, matches, requestContext); return { ...context, @@ -2229,6 +2263,7 @@ export function unstable_createStaticHandler( async function loadRouteData( request: Request, matches: AgnosticDataRouteMatch[], + requestContext: unknown, routeMatch?: AgnosticDataRouteMatch, pendingActionError?: RouteData ): Promise< @@ -2277,7 +2312,8 @@ export function unstable_createStaticHandler( matches, basename, true, - isRouteRequest + isRouteRequest, + requestContext ) ), ]); @@ -2580,7 +2616,8 @@ async function callLoaderOrAction( matches: AgnosticDataRouteMatch[], basename = "/", isStaticRequest: boolean = false, - isRouteRequest: boolean = false + isRouteRequest: boolean = false, + requestContext?: unknown ): Promise { let resultType; let result; @@ -2599,7 +2636,7 @@ async function callLoaderOrAction( ); result = await Promise.race([ - handler({ request, params: match.params }), + handler({ request, params: match.params, context: requestContext }), abortPromise, ]); @@ -2968,12 +3005,10 @@ function getInternalRouterError( pathname, routeId, method, - message, }: { pathname?: string; routeId?: string; method?: string; - message?: string; } = {} ) { let statusText = "Unknown Server Error"; diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 9cb192a747..7faeea8913 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -88,6 +88,7 @@ export interface Submission { interface DataFunctionArgs { request: Request; params: Params; + context?: any; } /**