Skip to content

Commit

Permalink
Add response stub to resource route calls when single fetch is enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Apr 30, 2024
1 parent 56e0a3f commit 1c641bf
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-peaches-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/server-runtime": patch
---

Pass `response` stub to resource route handlerså when single fetch is enabled
69 changes: 69 additions & 0 deletions integration/single-fetch-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,75 @@ test.describe("single-fetch", () => {
console.warn = oldConsoleWarn;
});

test("processes response stub onto resource routes returning raw data", async () => {
let fixture = await createFixture(
{
config: {
future: {
unstable_singleFetch: true,
},
},
files: {
...files,
"app/routes/resource.tsx": js`
import { json } from '@remix-run/node';
export function loader({ response }) {
response.status = 201;
response.headers.set('X-Response-Stub', 'yes')
return { message: "RESOURCE" };
}
`,
},
},
ServerMode.Development
);
let res = await fixture.requestResource("/resource");
expect(res.status).toBe(201);
expect(res.headers.get("X-Response-Stub")).toBe("yes");
expect(await res.json()).toEqual({
message: "RESOURCE",
});
});

test("processes response stub onto resource routes returning responses", async () => {
let fixture = await createFixture(
{
config: {
future: {
unstable_singleFetch: true,
},
},
files: {
...files,
"app/routes/resource.tsx": js`
import { json } from '@remix-run/node';
export function loader({ response }) {
response.status = 200; // ignored
response.headers.set('X-Response-Stub', 'yes')
return json({ message: "RESOURCE" }, {
// This one takes precedence
status: 201,
headers: {
'X-Response': 'yes'
},
});
}
`,
},
},
ServerMode.Development
);
let res = await fixture.requestResource("/resource");
expect(res.status).toBe(201);
expect(res.headers.get("X-Response")).toBe("yes");
expect(res.headers.get("X-Response-Stub")).toBe("yes");
expect(await res.json()).toEqual({
message: "RESOURCE",
});
});

test.describe("client loaders", () => {
test("when no routes have client loaders", async ({ page }) => {
let fixture = await createFixture(
Expand Down
42 changes: 34 additions & 8 deletions packages/remix-server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ import {
getResponseStubs,
getSingleFetchDataStrategy,
getSingleFetchRedirect,
getSingleFetchResourceRouteDataStrategy,
mergeResponseStubs,
singleFetchAction,
singleFetchLoaders,
SingleFetchRedirectSymbol,
} from "./single-fetch";
import { resourceRouteJsonWarning } from "./deprecations";
import { ResponseStubOperationsSymbol } from "./routeModules";

export type RequestHandler = (
request: Request,
Expand Down Expand Up @@ -570,12 +572,22 @@ async function handleResourceRequest(
handleError: (err: unknown) => void
) {
try {
let responseStubs = build.future.unstable_singleFetch
? getResponseStubs()
: {};
// Note we keep the routeId here to align with the Remix handling of
// resource routes which doesn't take ?index into account and just takes
// the leaf match
let response = await staticHandler.queryRoute(request, {
routeId,
requestContext: loadContext,
...(build.future.unstable_singleFetch
? {
unstable_dataStrategy: getSingleFetchResourceRouteDataStrategy({
responseStubs,
}),
}
: null),
});

if (typeof response === "object") {
Expand All @@ -586,14 +598,28 @@ async function handleResourceRequest(
);
}

if (build.future.unstable_singleFetch && !isResponse(response)) {
console.warn(
resourceRouteJsonWarning(
request.method === "GET" ? "loader" : "action",
routeId
)
);
response = json(response);
if (build.future.unstable_singleFetch) {
let stub = responseStubs[routeId];
if (isResponse(response)) {
// Merge directly onto the response if one was returned
let ops = stub[ResponseStubOperationsSymbol];
for (let [op, ...args] of ops) {
// @ts-expect-error
response.headers[op](...args);
}
} else {
console.warn(
resourceRouteJsonWarning(
request.method === "GET" ? "loader" : "action",
routeId
)
);
// Otherwise just create a response using the ResponseStub fields
response = json(response, {
status: stub.status || 200,
headers: stub.headers,
});
}
}

// callRouteLoader/callRouteAction always return responses (w/o single fetch).
Expand Down
31 changes: 29 additions & 2 deletions packages/remix-server-runtime/single-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
StaticHandler,
unstable_DataStrategyFunctionArgs as DataStrategyFunctionArgs,
unstable_DataStrategyFunction as DataStrategyFunction,
StaticHandlerContext,
} from "@remix-run/router";
import {
Expand Down Expand Up @@ -49,7 +50,7 @@ export function getSingleFetchDataStrategy(
isActionDataRequest,
loadRouteIds,
}: { isActionDataRequest?: boolean; loadRouteIds?: string[] } = {}
) {
): DataStrategyFunction {
return async ({ request, matches }: DataStrategyFunctionArgs) => {
// Don't call loaders on action data requests
if (isActionDataRequest && request.method === "GET") {
Expand Down Expand Up @@ -102,6 +103,32 @@ export function getSingleFetchDataStrategy(
};
}

export function getSingleFetchResourceRouteDataStrategy({
responseStubs,
}: {
responseStubs: ReturnType<typeof getResponseStubs>;
}): DataStrategyFunction {
return async ({ matches }: DataStrategyFunctionArgs) => {
let results = await Promise.all(
matches.map(async (match) => {
let responseStub = match.shouldLoad
? responseStubs[match.route.id]
: null;
let result = await match.resolve(async (handler) => {
// Cast `ResponseStubImpl -> ResponseStub` to hide the symbol in userland
let ctx: DataStrategyCtx = {
response: responseStub as ResponseStub,
};
let data = await handler(ctx);
return { type: "data", result: data };
});
return result;
})
);
return results;
};
}

export async function singleFetchAction(
serverMode: ServerMode,
staticHandler: StaticHandler,
Expand All @@ -128,7 +155,7 @@ export async function singleFetchAction(
}),
});

// Unlike `handleDataRequest`, when singleFetch is enabled, queryRoute does
// Unlike `handleDataRequest`, when singleFetch is enabled, query does
// let non-Response return values through
if (isResponse(result)) {
return {
Expand Down

0 comments on commit 1c641bf

Please sign in to comment.