Skip to content

Commit 1d778a7

Browse files
committed
feat(worker-runtime): runtime v5 release candidate
- added support for SPA modec- Disabled worker runtime APIs when single fetch is enabled BREAKING CHANGE
1 parent e4e673d commit 1d778a7

File tree

6 files changed

+271
-14
lines changed

6 files changed

+271
-14
lines changed

packages/worker-runtime/src/utils/__test__/handle-request.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('handleRequest', () => {
99
const routes = {
1010
route1: {
1111
id: 'route1',
12+
hasWorkerLoader: true,
1213
module: {
1314
workerLoader: vi.fn(() => Promise.resolve({ message: 'Hello, world!' })),
1415
},
@@ -31,6 +32,7 @@ describe('handleRequest', () => {
3132
const routes = {
3233
route1: {
3334
id: 'route1',
35+
hasWorkerLoader: true,
3436
module: {
3537
workerLoader: vi.fn().mockReturnValue(new Response('mock-response', { status: 200 })),
3638
},
@@ -53,6 +55,7 @@ describe('handleRequest', () => {
5355
const routes = {
5456
route1: {
5557
id: 'route1',
58+
hasWorkerAction: true,
5659
module: {
5760
workerAction: vi.fn(() => Promise.resolve({ message: 'Hello, world!' })),
5861
},
@@ -97,6 +100,7 @@ describe('handleRequest', () => {
97100
const routes = {
98101
route1: {
99102
id: 'route1',
103+
hasWorkerLoader: true,
100104
module: {
101105
workerLoader: vi.fn(() => Promise.reject(error)),
102106
},
@@ -124,6 +128,7 @@ describe('handleRequest', () => {
124128
const routes = {
125129
route1: {
126130
id: 'route1',
131+
hasWorkerAction: true,
127132
module: {
128133
workerAction: vi.fn().mockReturnValue(new Response(null, { status: 302, headers: { Location: '/route2' } })),
129134
},
@@ -147,6 +152,7 @@ describe('handleRequest', () => {
147152
const routes = {
148153
route1: {
149154
id: 'route1',
155+
hasWorkerAction: true,
150156
module: {
151157
workerAction: vi
152158
.fn()
@@ -176,6 +182,7 @@ describe('handleRequest', () => {
176182
const routes = {
177183
route1: {
178184
id: 'route1',
185+
hasWorkerAction: true,
179186
module: {
180187
workerAction: vi.fn().mockReturnValue(new Response('Remix response', { status: 200 })),
181188
},
@@ -200,6 +207,7 @@ describe('handleRequest', () => {
200207
const routes = {
201208
route1: {
202209
id: 'route1',
210+
hasWorkerAction: true,
203211
module: {
204212
workerAction: mockWorkerAction,
205213
},
@@ -225,6 +233,7 @@ describe('handleRequest', () => {
225233
const routes = {
226234
route1: {
227235
id: 'route1',
236+
hasWorkerAction: true,
228237
module: {
229238
workerAction: mockWorkerAction,
230239
},
@@ -254,6 +263,7 @@ describe('handleRequest', () => {
254263
const routes = {
255264
route1: {
256265
id: 'route1',
266+
hasWorkerAction: true,
257267
module: {
258268
workerAction: mockWorkerAction,
259269
},
@@ -283,6 +293,7 @@ describe('handleRequest', () => {
283293
const routes = {
284294
route1: {
285295
id: 'route1',
296+
hasWorkerAction: true,
286297
module: {
287298
workerAction: mockWorkerAction,
288299
},
@@ -326,6 +337,7 @@ describe('handleRequest', () => {
326337
const routes = {
327338
route1: {
328339
id: 'route1',
340+
hasWorkerAction: true,
329341
module: {
330342
workerAction: mockWorkerAction,
331343
},
@@ -364,6 +376,7 @@ describe('handleRequest', () => {
364376
const routes = {
365377
route1: {
366378
id: 'route1',
379+
hasWorkerAction: true,
367380
module: {
368381
workerAction: mockWorkerAction,
369382
},
@@ -392,6 +405,7 @@ describe('handleRequest', () => {
392405
const routes = {
393406
route1: {
394407
id: 'route1',
408+
hasWorkerAction: true,
395409
module: {
396410
workerAction: mockWorkerAction,
397411
},
@@ -420,6 +434,7 @@ describe('handleRequest', () => {
420434
const routes = {
421435
route1: {
422436
id: 'route1',
437+
hasWorkerLoader: true,
423438
module: {
424439
workerLoader: mockWorkerLoader,
425440
},
@@ -446,6 +461,7 @@ describe('handleRequest', () => {
446461
const routes = {
447462
route1: {
448463
id: 'route1',
464+
hasWorkerLoader: true,
449465
module: {
450466
workerLoader: mockWorkerLoader,
451467
},
@@ -480,6 +496,7 @@ describe('handleRequest', () => {
480496
const routes = {
481497
route1: {
482498
id: 'route1',
499+
hasWorkerLoader: true,
483500
module: {
484501
workerLoader: mockWorkerLoader,
485502
},

packages/worker-runtime/src/utils/__test__/request.test.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isActionRequest,
99
stripDataParameter,
1010
stripIndexParameter,
11+
stripRouteParameter,
1112
} from '../request.js';
1213

1314
describe('clone', () => {
@@ -62,10 +63,20 @@ describe('stripDataParam', () => {
6263
});
6364
});
6465

66+
describe('stripRouteParam', () => {
67+
test('should remove _route parameter from the URL', () => {
68+
const request = new Request('https://example.com/test?_route=root');
69+
const stripped = stripRouteParameter(request);
70+
71+
expect(stripped.url).toBe('https://example.com/test');
72+
expect(stripped.headers).toEqual(request.headers);
73+
});
74+
});
75+
6576
describe('createArgumentsFrom', () => {
6677
test('should create an object with request, params, and context properties', () => {
6778
const event = {
68-
request: new Request('https://example.com/test?a=1&b=2&index&_data=test'),
79+
request: new Request('https://example.com/test?a=1&_route=test&b=2&index&_data=test'),
6980
} as FetchEvent;
7081
const loadContext = {} as WorkerLoadContext;
7182

@@ -85,6 +96,13 @@ describe('isActionRequest', () => {
8596
expect(isAction).toBeTruthy();
8697
});
8798

99+
test('should return true for clientAction requests in SPA mode', () => {
100+
const request = new Request('https://example.com/test?_route=test', { method: 'POST' });
101+
const isAction = isActionRequest(request, true);
102+
103+
expect(isAction).toBeTruthy();
104+
});
105+
88106
test('should return false for non-action requests', () => {
89107
const request = new Request('https://example.com/test?a=1&b=2');
90108
const isAction = isActionRequest(request);

packages/worker-runtime/src/utils/handle-request.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import type {
66
WorkerRouteManifest,
77
} from '@remix-pwa/dev/worker-build.js';
88
import { isRouteErrorResponse } from '@remix-run/router';
9-
import { ServerMode } from '@remix-run/server-runtime/dist/mode.js';
10-
import type { TypedResponse } from '@remix-run/server-runtime/dist/responses.js';
9+
10+
import type { TypedResponse } from './remix.js';
1111
import {
12-
createDeferredReadableStream,
1312
isDeferredData,
1413
isRedirectResponse,
1514
isRedirectStatusCode,
1615
isResponse,
1716
json,
1817
redirect,
19-
} from '@remix-run/server-runtime/dist/responses.js';
20-
18+
ServerMode,
19+
} from './remix.js';
2120
import { createArgumentsFrom, getURLParameters, isActionRequest, isLoaderRequest } from './request.js';
2221
import { errorResponseToJson, isRemixResponse } from './response.js';
22+
import { createDeferredReadableStream } from './unstable.js';
2323

2424
interface HandleRequestArgs {
2525
defaultHandler: DefaultFetchHandler;
@@ -47,6 +47,7 @@ interface HandleError {
4747

4848
/**
4949
* A FetchEvent handler for Remix.
50+
*
5051
* If the `event.request` has a worker loader/action defined, it will call it and return the response.
5152
* Otherwise, it will call the default handler...
5253
*/
@@ -57,11 +58,23 @@ export async function handleRequest({
5758
loadContext,
5859
routes,
5960
}: HandleRequestArgs): Promise<Response> {
60-
const isSPAMode = process.env.__REMIX_PWA_SPA_MODE === 'true';
61+
const isSPAMode = String(process.env.__REMIX_PWA_SPA_MODE) === 'true';
62+
const isSingleFetchMode = String(process.env.__REMIX_SINGLE_FETCH) === 'true';
6163

6264
const url = new URL(event.request.url);
63-
const routeId = url.searchParams.get('_data');
64-
// if the request is not a loader or action request, we call the default handler and the routeId will be undefined
65+
66+
let routeId: string | null;
67+
68+
if (!isSPAMode) {
69+
routeId = url.searchParams.get('_data');
70+
} else {
71+
routeId = url.searchParams.get('_route');
72+
}
73+
74+
if (isSingleFetchMode) routeId = null;
75+
76+
// if the request is not a loader or action request, we call
77+
// the default handler and the routeId will be undefined
6578
const route = routeId ? routes[routeId] : undefined;
6679
const _arguments = {
6780
request: event.request,
@@ -70,7 +83,7 @@ export async function handleRequest({
7083
};
7184

7285
try {
73-
if (isLoaderRequest(event.request, isSPAMode) && route?.module.workerLoader) {
86+
if (isLoaderRequest(event.request, isSPAMode) && route?.hasWorkerLoader && route?.module?.workerLoader) {
7487
return await handleLoader({
7588
event,
7689
loader: route.module.workerLoader,
@@ -80,7 +93,7 @@ export async function handleRequest({
8093
}).then(responseHandler);
8194
}
8295

83-
if (isActionRequest(event.request, isSPAMode) && route?.module?.workerAction) {
96+
if (isActionRequest(event.request, isSPAMode) && route?.hasWorkerAction && route?.module?.workerAction) {
8497
return await handleAction({
8598
event,
8699
action: route.module.workerAction,
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { DeferredData, TrackedPromise } from '@remix-run/router/dist/utils.js';
2+
3+
export type RedirectFunction = (url: string, init?: number | ResponseInit) => Response;
4+
export type JsonFunction = <Data>(data: Data, init?: number | ResponseInit) => Response;
5+
export type TypedResponse<T = unknown> = Omit<Response, 'json'> & {
6+
json(): Promise<T>;
7+
};
8+
9+
/**
10+
* The mode to use when running the server.
11+
*/
12+
export enum ServerMode {
13+
Development = 'development',
14+
Production = 'production',
15+
Test = 'test',
16+
}
17+
18+
export function isServerMode(value: any): value is ServerMode {
19+
return value === ServerMode.Development || value === ServerMode.Production || value === ServerMode.Test;
20+
}
21+
22+
export function isDeferredData(value: any): value is DeferredData {
23+
const deferred: DeferredData = value;
24+
return (
25+
deferred &&
26+
typeof deferred === 'object' &&
27+
typeof deferred.data === 'object' &&
28+
typeof deferred.subscribe === 'function' &&
29+
typeof deferred.cancel === 'function' &&
30+
typeof deferred.resolveData === 'function'
31+
);
32+
}
33+
34+
export function isResponse(value: any): value is Response {
35+
return (
36+
value != null &&
37+
typeof value.status === 'number' &&
38+
typeof value.statusText === 'string' &&
39+
typeof value.headers === 'object' &&
40+
typeof value.body !== 'undefined'
41+
);
42+
}
43+
44+
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
45+
export function isRedirectStatusCode(statusCode: number): boolean {
46+
return redirectStatusCodes.has(statusCode);
47+
}
48+
export function isRedirectResponse(response: Response): boolean {
49+
return isRedirectStatusCode(response.status);
50+
}
51+
52+
export function isTrackedPromise(value: any): value is TrackedPromise {
53+
return value != null && typeof value.then === 'function' && value._tracked === true;
54+
}
55+
56+
/**
57+
* A redirect response. Sets the status code and the `Location` header.
58+
* Defaults to "302 Found".
59+
*/
60+
export const redirect: RedirectFunction = (url, init = 302) => {
61+
let responseInit = init;
62+
if (typeof responseInit === 'number') {
63+
responseInit = { status: responseInit };
64+
} else if (typeof responseInit.status === 'undefined') {
65+
responseInit.status = 302;
66+
}
67+
68+
const headers = new Headers(responseInit.headers);
69+
headers.set('Location', url);
70+
71+
return new Response(null, {
72+
...responseInit,
73+
headers,
74+
});
75+
};
76+
77+
/**
78+
* This is a shortcut for creating `application/json` responses. Converts `data`
79+
* to JSON and sets the `Content-Type` header.
80+
*/
81+
export const json: JsonFunction = (data, init = {}) => {
82+
const responseInit = typeof init === 'number' ? { status: init } : init;
83+
84+
const headers = new Headers(responseInit.headers);
85+
if (!headers.has('Content-Type')) {
86+
headers.set('Content-Type', 'application/json; charset=utf-8');
87+
}
88+
89+
return new Response(JSON.stringify(data), {
90+
...responseInit,
91+
headers,
92+
});
93+
};
94+
95+
export function sanitizeError<T = unknown>(error: T, serverMode: ServerMode) {
96+
if (error instanceof Error && serverMode !== ServerMode.Development) {
97+
const sanitized = new Error('Unexpected Server Error');
98+
sanitized.stack = undefined;
99+
return sanitized;
100+
}
101+
return error;
102+
}
103+
104+
// must be type alias due to inference issues on interfaces
105+
// https://github.com/microsoft/TypeScript/issues/15300
106+
export type SerializedError = {
107+
message: string;
108+
stack?: string;
109+
};
110+
111+
export function serializeError(error: Error, serverMode: ServerMode): SerializedError {
112+
const sanitized = sanitizeError(error, serverMode);
113+
return {
114+
message: sanitized.message,
115+
stack: sanitized.stack,
116+
};
117+
}

0 commit comments

Comments
 (0)