Skip to content

Commit

Permalink
feat: plain and web adapters (#483)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Aug 2, 2023
1 parent d351ba9 commit 368493a
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 15 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"iron-webcrypto": "^0.8.0",
"radix3": "^1.0.1",
"ufo": "^1.1.2",
"uncrypto": "^0.1.3"
"uncrypto": "^0.1.3",
"unenv": "^1.6.1"
},
"devDependencies": {
"0x": "^5.5.0",
Expand All @@ -64,4 +65,4 @@
"zod": "^3.21.4"
},
"packageManager": "pnpm@8.6.9"
}
}
38 changes: 36 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export * from "./node";

export { type WebHandler, toWebHandler } from "./web";

export {
type PlainHandler,
type PlainRequest,
type PlainResponse,
toPlainHandler,
} from "./plain";
8 changes: 4 additions & 4 deletions src/node.ts → src/adapters/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type {
IncomingMessage as NodeIncomingMessage,
ServerResponse as NodeServerResponse,
} from "node:http";
import { App } from "./app";
import { createError, isError, sendError } from "./error";
import { createEvent, eventHandler, isEventHandler } from "./event";
import { EventHandler, EventHandlerResponse } from "./types";
import { App } from "../app";
import { createError, isError, sendError } from "../error";
import { createEvent, eventHandler, isEventHandler } from "../event";
import { EventHandler, EventHandlerResponse } from "../types";

// Node.js
export type {
Expand Down
132 changes: 132 additions & 0 deletions src/adapters/plain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { IncomingMessage as NodeIncomingMessage } from "unenv/runtime/node/http/_request";
import { ServerResponse as NodeServerResponse } from "unenv/runtime/node/http/_response";
import type { App } from "../app";
import type { HTTPMethod } from "../types";
import { createError, isError, sendError } from "../error";
import { H3Event, createEvent } from "../event";
import { splitCookiesString } from "../utils";

export interface PlainRequest {
_eventOverrides?: Partial<H3Event>;
context?: Record<string, unknown>;

method: string;
path: string;
headers: HeadersInit;
body?: null | BodyInit;
}

export interface PlainResponse {
status: number;
statusText: string;
headers: [string, string][];
body?: unknown;
}

export type PlainHandler = (request: PlainRequest) => Promise<PlainResponse>;

/** @experimental */
export function toPlainHandler(app: App) {
const handler: PlainHandler = (request) => {
return _handlePlainRequest(app, request);
};
return handler;
}

// --- Internal ---

export async function _handlePlainRequest(app: App, request: PlainRequest) {
// Normalize request
const path = request.path;
const method = (request.method || "GET").toUpperCase() as HTTPMethod;
const headers = new Headers(request.headers);

// Shim for Node.js request and response objects
// TODO: Remove in next major version
const nodeReq = new NodeIncomingMessage();
const nodeRes = new NodeServerResponse(nodeReq);

// Fill node request properties
nodeReq.method = method;
nodeReq.url = path;
// TODO: Normalize with array merge and lazy getter
nodeReq.headers = Object.fromEntries(headers.entries());

// Create new event
const event = createEvent(nodeReq, nodeRes);

// Fill internal event properties
event._method = method;
event._path = path;
event._headers = headers;
if (request.body) {
event._body = request.body;
}
if (request._eventOverrides) {
Object.assign(event, request._eventOverrides);
}
if (request.context) {
Object.assign(event.context, request.context);
}

// Run app handler logic
try {
await app.handler(event);
} catch (_error: any) {
const error = createError(_error);
if (!isError(_error)) {
error.unhandled = true;
}
if (app.options.onError) {
await app.options.onError(error, event);
}
if (!event.handled) {
if (error.unhandled || error.fatal) {
console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); // eslint-disable-line no-console
}
await sendError(event, error, !!app.options.debug);
}
}

return {
status: nodeRes.statusCode,
statusText: nodeRes.statusMessage,
headers: _normalizeUnenvHeaders(nodeRes._headers),
body: nodeRes._data,
};
}

function _normalizeUnenvHeaders(
input: Record<string, undefined | string | number | string[]>
) {
const headers: [string, string][] = [];
const cookies: string[] = [];

for (const _key in input) {
const key = _key.toLowerCase();

if (key === "set-cookie") {
cookies.push(
...splitCookiesString(input["set-cookie"] as string | string[])
);
continue;
}

const value = input[key];
if (Array.isArray(value)) {
for (const _value of value) {
headers.push([key, _value]);
}
} else if (value !== undefined) {
headers.push([key, String(value)]);
}
}

if (cookies.length > 0) {
for (const cookie of cookies) {
headers.push(["set-cookie", cookie]);
}
}

return headers;
}
43 changes: 43 additions & 0 deletions src/adapters/web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { App } from "../app";
import { _handlePlainRequest } from "./plain";

export type WebHandler = (
request: Request,
context?: Record<string, unknown>
) => Promise<Response>;

/** @experimental */
export function toWebHandler(app: App) {
const webHandler: WebHandler = (request, context) => {
return _handleWebRequest(app, request, context);
};

return webHandler;
}

// --- Internal ---

async function _handleWebRequest(
app: App,
request: Request,
context?: Record<string, unknown>
) {
const url = new URL(request.url);
const res = await _handlePlainRequest(app, {
_eventOverrides: {
_request: request,
_url: url,
},
context,
method: request.method,
path: url.pathname + url.search,
headers: request.headers,
body: request.body,
});

return new Response(res.body as BodyInit, {
status: res.status,
statusText: res.statusText,
headers: res.headers,
});
}
2 changes: 1 addition & 1 deletion src/event/event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IncomingHttpHeaders } from "node:http";
import type { H3EventContext, HTTPMethod, EventHandlerRequest } from "../types";
import type { NodeIncomingMessage, NodeServerResponse } from "../node";
import type { NodeIncomingMessage, NodeServerResponse } from "../adapters/node";
import { getRequestURL, sendWebResponse } from "../utils";

// TODO: Dedup from body.ts
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from "./app";
export * from "./error";
export * from "./event";
export * from "./node";
export * from "./utils";
export * from "./router";
export * from "./types";
export * from "./adapters";
14 changes: 9 additions & 5 deletions src/utils/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export function deleteCookie(
* Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
* @source https://github.com/nfriedly/set-cookie-parser/blob/3eab8b7d5d12c8ed87832532861c1a35520cf5b3/lib/set-cookie.js#L144
*/
export function splitCookiesString(cookiesString: string): string[] {
export function splitCookiesString(cookiesString: string | string[]): string[] {
if (Array.isArray(cookiesString)) {
return cookiesString.flatMap((c) => splitCookiesString(c));
}

if (typeof cookiesString !== "string") {
return [];
}
Expand All @@ -99,18 +103,18 @@ export function splitCookiesString(cookiesString: string): string[] {
let nextStart;
let cookiesSeparatorFound;

function skipWhitespace() {
const skipWhitespace = () => {
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
pos += 1;
}
return pos < cookiesString.length;
}
};

function notSpecialChar() {
const notSpecialChar = () => {
ch = cookiesString.charAt(pos);

return ch !== "=" && ch !== ";" && ch !== ",";
}
};

while (pos < cookiesString.length) {
start = pos;
Expand Down
Loading

0 comments on commit 368493a

Please sign in to comment.