Skip to content

Commit 45bf188

Browse files
committed
feat: support some response APIs
1 parent 437638f commit 45bf188

File tree

10 files changed

+231
-15
lines changed

10 files changed

+231
-15
lines changed

.changeset/two-guests-argue.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@modern-js/runtime': patch
3+
'@modern-js/runtime-utils': patch
4+
---
5+
6+
feat: Add response APIs to support setting response headers, status codes, and redirects
7+
feat: 添加一些响应的 API,可以设置响应头,状态码,及重定向
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { storage } from '@modern-js/runtime-utils/node';
2+
export const getResponseProxy = () => {
3+
const context = storage.useContext();
4+
return context?.responseProxy;
5+
};
6+
7+
export const setHeaders = (headers: Record<string, string>) => {
8+
const responseProxy = getResponseProxy();
9+
Object.entries(headers).forEach(([key, value]) => {
10+
responseProxy!.headers[key] = value;
11+
});
12+
};
13+
14+
export const setStatus = (status: number) => {
15+
const responseProxy = getResponseProxy();
16+
responseProxy!.status = status;
17+
};
18+
19+
export const redirect = (url: string, init?: number | ResponseInit) => {
20+
const status =
21+
init === undefined
22+
? 307
23+
: typeof init === 'number'
24+
? init
25+
: (init.status ?? 307);
26+
const headers =
27+
init === undefined
28+
? {}
29+
: typeof init === 'number'
30+
? {}
31+
: (init.headers ?? {});
32+
33+
setStatus(status);
34+
setHeaders({
35+
Location: url,
36+
...(init && typeof init === 'object'
37+
? Object.fromEntries(
38+
Object.entries(headers).map(([k, v]) => [k, String(v)]),
39+
)
40+
: {}),
41+
});
42+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const getResponseProxy = () => {
2+
return null;
3+
};
4+
export const setHeaders = (headers: Record<string, string>) => {};
5+
export const setStatus = (status: number) => {};
6+
export const redirect = (url: string, init?: number | ResponseInit) => {};

packages/runtime/plugin-runtime/src/core/server/requestHandler.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export type CreateRequestHandler = (
4848

4949
type ResponseProxy = {
5050
headers: Record<string, string>;
51-
code: number;
51+
status: number;
5252
};
5353

5454
function createSSRContext(
@@ -141,7 +141,7 @@ function createSSRContext(
141141
responseProxy.headers[key] = value;
142142
},
143143
status(code) {
144-
responseProxy.code = code;
144+
responseProxy.status = code;
145145
},
146146
locals: locals || {},
147147
},
@@ -159,8 +159,17 @@ export const createRequestHandler: CreateRequestHandler = async (
159159
) => {
160160
const requestHandler: RequestHandler = async (request, options) => {
161161
const headersData = parseHeaders(request);
162+
const responseProxy: ResponseProxy = {
163+
headers: {},
164+
status: -1,
165+
};
162166
return storage.run(
163-
{ headers: headersData, request, monitors: options.monitors },
167+
{
168+
headers: headersData,
169+
request,
170+
monitors: options.monitors,
171+
responseProxy,
172+
},
164173
async () => {
165174
const Root = createRoot();
166175

@@ -184,11 +193,6 @@ export const createRequestHandler: CreateRequestHandler = async (
184193
return init?.(context);
185194
};
186195

187-
const responseProxy: ResponseProxy = {
188-
headers: {},
189-
code: -1,
190-
};
191-
192196
const ssrContext = createSSRContext(request, {
193197
...options,
194198
responseProxy,
@@ -277,9 +281,9 @@ export const createRequestHandler: CreateRequestHandler = async (
277281
response.headers.set(key, value);
278282
});
279283

280-
if (responseProxy.code !== -1) {
284+
if (responseProxy.status !== -1) {
281285
return new Response(response.body, {
282-
status: responseProxy.code,
286+
status: responseProxy.status,
283287
headers: response.headers,
284288
});
285289
}

packages/runtime/plugin-runtime/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type { RuntimeUserConfig } from './config';
1010

1111
export { getMonitors } from './core/context/monitors';
1212
export { getRequest } from './core/context/request';
13+
export { setHeaders, setStatus, redirect } from './core/context/response';
1314

1415
export {
1516
createApp,
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { getRequest } from './core/context/request';
22
export { getMonitors } from './core/context/monitors';
3+
export { setHeaders, setStatus, redirect } from './core/context/response';

packages/toolkit/runtime-utils/src/universal/async_storage.server.ts

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ const storage = createStorage<{
5151
monitors?: Monitors;
5252
headers?: IncomingHttpHeaders;
5353
request?: Request;
54+
responseProxy?: {
55+
headers: Record<string, string>;
56+
status: number;
57+
};
5458
}>();
5559

5660
type Storage = typeof storage;

tests/integration/rsc-ssr-app/src/client-component-root/App.tsx

+40-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,51 @@
11
'use client';
22
import 'client-only';
3-
import { useRuntimeContext } from '@modern-js/runtime';
3+
import {
4+
getRequest,
5+
redirect,
6+
setHeaders,
7+
setStatus,
8+
useRuntimeContext,
9+
} from '@modern-js/runtime';
410
import './App.css';
5-
import { getRequest } from '@modern-js/runtime';
611
import { Counter } from './components/Counter';
712

13+
const handleResponse = (responseType: string) => {
14+
switch (responseType) {
15+
case 'headers':
16+
setHeaders({ 'x-test': 'test-value' });
17+
return { message: 'headers set' };
18+
19+
case 'status':
20+
setStatus(418);
21+
return { message: 'status set' };
22+
23+
case 'redirect':
24+
redirect('/server-component-root', 307);
25+
return null;
26+
27+
case 'redirect-with-headers':
28+
redirect('/server-component-root', {
29+
status: 301,
30+
headers: {
31+
'x-redirect-test': 'test',
32+
},
33+
});
34+
return null;
35+
36+
default:
37+
return { message: 'invalid type' };
38+
}
39+
};
40+
841
const App = () => {
942
const context = useRuntimeContext();
1043
const request = getRequest();
11-
console.log(typeof request?.url);
44+
const url = new URL(request.url);
45+
const responseType = url.searchParams.get('type');
46+
if (responseType) {
47+
handleResponse(responseType);
48+
}
1249
return (
1350
<>
1451
<div className="container">

tests/integration/rsc-ssr-app/src/server-component-root/App.tsx

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,48 @@
11
import 'server-only';
2-
import { getRequest } from '@modern-js/runtime';
2+
import { getRequest, redirect, setHeaders } from '@modern-js/runtime';
3+
import { setStatus } from '@modern-js/runtime';
34
import { Suspense } from 'react';
45
import styles from './App.module.less';
56
import Suspended from './Suspended';
67
import { Counter } from './components/Counter';
78
import { getCountState } from './components/ServerState';
89

10+
const handleResponse = (responseType: string) => {
11+
switch (responseType) {
12+
case 'headers':
13+
setHeaders({ 'x-test': 'test-value' });
14+
return { message: 'headers set' };
15+
16+
case 'status':
17+
setStatus(418);
18+
return { message: 'status set' };
19+
20+
case 'redirect':
21+
redirect('/client-component-root', 307);
22+
return null;
23+
24+
case 'redirect-with-headers':
25+
redirect('/client-component-root', {
26+
status: 301,
27+
headers: {
28+
'x-redirect-test': 'test',
29+
},
30+
});
31+
return null;
32+
33+
default:
34+
return { message: 'invalid type' };
35+
}
36+
};
37+
938
const App = ({ name }: { name: string }) => {
1039
const request = getRequest();
11-
console.log(typeof request?.url);
40+
const url = new URL(request.url);
41+
const responseType = url.searchParams.get('type');
42+
if (responseType) {
43+
handleResponse(responseType);
44+
}
45+
1246
const countStateFromServer = getCountState();
1347
return (
1448
<div className={styles.root}>

tests/integration/rsc-ssr-app/tests/index.test.ts

+80
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ describe('dev', () => {
5454
renderClientRootPageCorrectly({ baseUrl, appPort, page }));
5555
it('should render page with context correctly', () =>
5656
renderPageWithContext({ baseUrl, appPort, page }));
57+
it('should support response api', () =>
58+
supportResponseAPIForClientRoot({ baseUrl, appPort, page }));
5759
});
5860

5961
describe('server component root', () => {
@@ -62,6 +64,8 @@ describe('dev', () => {
6264
renderServerRootPageCorrectly({ baseUrl, appPort, page }));
6365
it('should support client and server actions', () =>
6466
supportServerAction({ baseUrl, appPort, page }));
67+
it('should support response api', () =>
68+
supportResponseAPIForServerRoot({ baseUrl, appPort, page }));
6569
});
6670
});
6771

@@ -110,6 +114,8 @@ describe('build', () => {
110114
renderClientRootPageCorrectly({ baseUrl, appPort, page }));
111115
it('should render page with context correctly', () =>
112116
renderPageWithContext({ baseUrl, appPort, page }));
117+
it('should support response api', () =>
118+
supportResponseAPIForClientRoot({ baseUrl, appPort, page }));
113119
});
114120

115121
describe('server component root', () => {
@@ -118,6 +124,8 @@ describe('build', () => {
118124
renderServerRootPageCorrectly({ baseUrl, appPort, page }));
119125
it('should support server action', () =>
120126
supportServerAction({ baseUrl, appPort, page }));
127+
it('should support response api', () =>
128+
supportResponseAPIForServerRoot({ baseUrl, appPort, page }));
121129
});
122130
});
123131

@@ -181,3 +189,75 @@ async function supportServerAction({ baseUrl, appPort, page }: TestOptions) {
181189
serverCount = await page.$eval('.server-count', el => el.textContent);
182190
expect(serverCount).toBe('1');
183191
}
192+
193+
async function supportResponseAPIForServerRoot({
194+
baseUrl,
195+
appPort,
196+
page,
197+
}: TestOptions) {
198+
const headersRes = await fetch(
199+
`http://127.0.0.1:${appPort}/${baseUrl}?type=headers`,
200+
);
201+
expect(headersRes.headers.get('x-test')).toBe('test-value');
202+
203+
// Test setStatus
204+
const statusRes = await fetch(
205+
`http://127.0.0.1:${appPort}/${baseUrl}?type=status`,
206+
);
207+
expect(statusRes.status).toBe(418);
208+
209+
// Test redirect with status code
210+
const redirectRes = await fetch(
211+
`http://127.0.0.1:${appPort}/${baseUrl}?type=redirect`,
212+
{ redirect: 'manual' },
213+
);
214+
expect(redirectRes.status).toBe(307);
215+
expect(redirectRes.headers.get('location')).toBe('/client-component-root');
216+
217+
// Test redirect with init object
218+
const redirectWithHeadersRes = await fetch(
219+
`http://127.0.0.1:${appPort}/${baseUrl}?type=redirect-with-headers`,
220+
{ redirect: 'manual' },
221+
);
222+
expect(redirectWithHeadersRes.status).toBe(301);
223+
expect(redirectWithHeadersRes.headers.get('location')).toBe(
224+
'/client-component-root',
225+
);
226+
expect(redirectWithHeadersRes.headers.get('x-redirect-test')).toBe('test');
227+
}
228+
229+
async function supportResponseAPIForClientRoot({
230+
baseUrl,
231+
appPort,
232+
page,
233+
}: TestOptions) {
234+
const headersRes = await fetch(
235+
`http://127.0.0.1:${appPort}/${baseUrl}?type=headers`,
236+
);
237+
expect(headersRes.headers.get('x-test')).toBe('test-value');
238+
239+
// Test setStatus
240+
const statusRes = await fetch(
241+
`http://127.0.0.1:${appPort}/${baseUrl}?type=status`,
242+
);
243+
expect(statusRes.status).toBe(418);
244+
245+
// Test redirect with status code
246+
const redirectRes = await fetch(
247+
`http://127.0.0.1:${appPort}/${baseUrl}?type=redirect`,
248+
{ redirect: 'manual' },
249+
);
250+
expect(redirectRes.status).toBe(307);
251+
expect(redirectRes.headers.get('location')).toBe('/server-component-root');
252+
253+
// Test redirect with init object
254+
const redirectWithHeadersRes = await fetch(
255+
`http://127.0.0.1:${appPort}/${baseUrl}?type=redirect-with-headers`,
256+
{ redirect: 'manual' },
257+
);
258+
expect(redirectWithHeadersRes.status).toBe(301);
259+
expect(redirectWithHeadersRes.headers.get('location')).toBe(
260+
'/server-component-root',
261+
);
262+
expect(redirectWithHeadersRes.headers.get('x-redirect-test')).toBe('test');
263+
}

0 commit comments

Comments
 (0)