Skip to content

Commit

Permalink
feat(worker-runtime): runtime v5 release candidate
Browse files Browse the repository at this point in the history
- added support for SPA modec- Disabled worker runtime APIs when single fetch is enabled

BREAKING CHANGE
  • Loading branch information
ShafSpecs committed Oct 1, 2024
1 parent e4e673d commit 1d778a7
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 14 deletions.
17 changes: 17 additions & 0 deletions packages/worker-runtime/src/utils/__test__/handle-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerLoader: true,
module: {
workerLoader: vi.fn(() => Promise.resolve({ message: 'Hello, world!' })),
},
Expand All @@ -31,6 +32,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerLoader: true,
module: {
workerLoader: vi.fn().mockReturnValue(new Response('mock-response', { status: 200 })),
},
Expand All @@ -53,6 +55,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: vi.fn(() => Promise.resolve({ message: 'Hello, world!' })),
},
Expand Down Expand Up @@ -97,6 +100,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerLoader: true,
module: {
workerLoader: vi.fn(() => Promise.reject(error)),
},
Expand Down Expand Up @@ -124,6 +128,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: vi.fn().mockReturnValue(new Response(null, { status: 302, headers: { Location: '/route2' } })),
},
Expand All @@ -147,6 +152,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: vi
.fn()
Expand Down Expand Up @@ -176,6 +182,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: vi.fn().mockReturnValue(new Response('Remix response', { status: 200 })),
},
Expand All @@ -200,6 +207,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand All @@ -225,6 +233,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand Down Expand Up @@ -254,6 +263,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand Down Expand Up @@ -283,6 +293,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand Down Expand Up @@ -326,6 +337,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand Down Expand Up @@ -364,6 +376,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand Down Expand Up @@ -392,6 +405,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerAction: true,
module: {
workerAction: mockWorkerAction,
},
Expand Down Expand Up @@ -420,6 +434,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerLoader: true,
module: {
workerLoader: mockWorkerLoader,
},
Expand All @@ -446,6 +461,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerLoader: true,
module: {
workerLoader: mockWorkerLoader,
},
Expand Down Expand Up @@ -480,6 +496,7 @@ describe('handleRequest', () => {
const routes = {
route1: {
id: 'route1',
hasWorkerLoader: true,
module: {
workerLoader: mockWorkerLoader,
},
Expand Down
20 changes: 19 additions & 1 deletion packages/worker-runtime/src/utils/__test__/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isActionRequest,
stripDataParameter,
stripIndexParameter,
stripRouteParameter,
} from '../request.js';

describe('clone', () => {
Expand Down Expand Up @@ -62,10 +63,20 @@ describe('stripDataParam', () => {
});
});

describe('stripRouteParam', () => {
test('should remove _route parameter from the URL', () => {
const request = new Request('https://example.com/test?_route=root');
const stripped = stripRouteParameter(request);

expect(stripped.url).toBe('https://example.com/test');
expect(stripped.headers).toEqual(request.headers);
});
});

describe('createArgumentsFrom', () => {
test('should create an object with request, params, and context properties', () => {
const event = {
request: new Request('https://example.com/test?a=1&b=2&index&_data=test'),
request: new Request('https://example.com/test?a=1&_route=test&b=2&index&_data=test'),
} as FetchEvent;
const loadContext = {} as WorkerLoadContext;

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

test('should return true for clientAction requests in SPA mode', () => {
const request = new Request('https://example.com/test?_route=test', { method: 'POST' });
const isAction = isActionRequest(request, true);

expect(isAction).toBeTruthy();
});

test('should return false for non-action requests', () => {
const request = new Request('https://example.com/test?a=1&b=2');
const isAction = isActionRequest(request);
Expand Down
33 changes: 23 additions & 10 deletions packages/worker-runtime/src/utils/handle-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import type {
WorkerRouteManifest,
} from '@remix-pwa/dev/worker-build.js';
import { isRouteErrorResponse } from '@remix-run/router';
import { ServerMode } from '@remix-run/server-runtime/dist/mode.js';
import type { TypedResponse } from '@remix-run/server-runtime/dist/responses.js';

import type { TypedResponse } from './remix.js';
import {
createDeferredReadableStream,
isDeferredData,
isRedirectResponse,
isRedirectStatusCode,
isResponse,
json,
redirect,
} from '@remix-run/server-runtime/dist/responses.js';

ServerMode,
} from './remix.js';
import { createArgumentsFrom, getURLParameters, isActionRequest, isLoaderRequest } from './request.js';
import { errorResponseToJson, isRemixResponse } from './response.js';
import { createDeferredReadableStream } from './unstable.js';

interface HandleRequestArgs {
defaultHandler: DefaultFetchHandler;
Expand Down Expand Up @@ -47,6 +47,7 @@ interface HandleError {

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

const url = new URL(event.request.url);
const routeId = url.searchParams.get('_data');
// if the request is not a loader or action request, we call the default handler and the routeId will be undefined

let routeId: string | null;

if (!isSPAMode) {
routeId = url.searchParams.get('_data');
} else {
routeId = url.searchParams.get('_route');
}

if (isSingleFetchMode) routeId = null;

// if the request is not a loader or action request, we call
// the default handler and the routeId will be undefined
const route = routeId ? routes[routeId] : undefined;
const _arguments = {
request: event.request,
Expand All @@ -70,7 +83,7 @@ export async function handleRequest({
};

try {
if (isLoaderRequest(event.request, isSPAMode) && route?.module.workerLoader) {
if (isLoaderRequest(event.request, isSPAMode) && route?.hasWorkerLoader && route?.module?.workerLoader) {
return await handleLoader({
event,
loader: route.module.workerLoader,
Expand All @@ -80,7 +93,7 @@ export async function handleRequest({
}).then(responseHandler);
}

if (isActionRequest(event.request, isSPAMode) && route?.module?.workerAction) {
if (isActionRequest(event.request, isSPAMode) && route?.hasWorkerAction && route?.module?.workerAction) {
return await handleAction({
event,
action: route.module.workerAction,
Expand Down
117 changes: 117 additions & 0 deletions packages/worker-runtime/src/utils/remix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { DeferredData, TrackedPromise } from '@remix-run/router/dist/utils.js';

export type RedirectFunction = (url: string, init?: number | ResponseInit) => Response;
export type JsonFunction = <Data>(data: Data, init?: number | ResponseInit) => Response;
export type TypedResponse<T = unknown> = Omit<Response, 'json'> & {
json(): Promise<T>;
};

/**
* The mode to use when running the server.
*/
export enum ServerMode {
Development = 'development',
Production = 'production',
Test = 'test',
}

export function isServerMode(value: any): value is ServerMode {
return value === ServerMode.Development || value === ServerMode.Production || value === ServerMode.Test;
}

export function isDeferredData(value: any): value is DeferredData {
const deferred: DeferredData = value;
return (
deferred &&
typeof deferred === 'object' &&
typeof deferred.data === 'object' &&
typeof deferred.subscribe === 'function' &&
typeof deferred.cancel === 'function' &&
typeof deferred.resolveData === 'function'
);
}

export function isResponse(value: any): value is Response {
return (
value != null &&
typeof value.status === 'number' &&
typeof value.statusText === 'string' &&
typeof value.headers === 'object' &&
typeof value.body !== 'undefined'
);
}

const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
export function isRedirectStatusCode(statusCode: number): boolean {
return redirectStatusCodes.has(statusCode);
}
export function isRedirectResponse(response: Response): boolean {
return isRedirectStatusCode(response.status);
}

export function isTrackedPromise(value: any): value is TrackedPromise {
return value != null && typeof value.then === 'function' && value._tracked === true;
}

/**
* A redirect response. Sets the status code and the `Location` header.
* Defaults to "302 Found".
*/
export const redirect: RedirectFunction = (url, init = 302) => {
let responseInit = init;
if (typeof responseInit === 'number') {
responseInit = { status: responseInit };
} else if (typeof responseInit.status === 'undefined') {
responseInit.status = 302;
}

const headers = new Headers(responseInit.headers);
headers.set('Location', url);

return new Response(null, {
...responseInit,
headers,
});
};

/**
* This is a shortcut for creating `application/json` responses. Converts `data`
* to JSON and sets the `Content-Type` header.
*/
export const json: JsonFunction = (data, init = {}) => {
const responseInit = typeof init === 'number' ? { status: init } : init;

const headers = new Headers(responseInit.headers);
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json; charset=utf-8');
}

return new Response(JSON.stringify(data), {
...responseInit,
headers,
});
};

export function sanitizeError<T = unknown>(error: T, serverMode: ServerMode) {
if (error instanceof Error && serverMode !== ServerMode.Development) {
const sanitized = new Error('Unexpected Server Error');
sanitized.stack = undefined;
return sanitized;
}
return error;
}

// must be type alias due to inference issues on interfaces
// https://github.com/microsoft/TypeScript/issues/15300
export type SerializedError = {
message: string;
stack?: string;
};

export function serializeError(error: Error, serverMode: ServerMode): SerializedError {
const sanitized = sanitizeError(error, serverMode);
return {
message: sanitized.message,
stack: sanitized.stack,
};
}
Loading

0 comments on commit 1d778a7

Please sign in to comment.