Skip to content

Commit

Permalink
Improvements in the API (#121)
Browse files Browse the repository at this point in the history
* changeset

* Go

* Prettuer
  • Loading branch information
ardatan committed Sep 16, 2022
1 parent b29e4cd commit a67f447
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 105 deletions.
50 changes: 50 additions & 0 deletions .changeset/moody-coins-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
'@whatwg-node/server': minor
---

Improvements;

- `createServerAdapter` can now accept the request handler itself.

```ts
createServerAdapter(req => {
return new Response(`I got ${req.url}`)
})
```

Breaking Changes;

- `baseObject` in the configuration has been removed! Now you can pass `baseObject` itself but `baseObject` needs to implement a `handle` method that is exactly same with `handleRequest`.

```diff
- const myServerBaseObject = {...}
+ const myServerBaseObject = {
+ handle(req) {/*...*/}
+ }

- const adapter = createServerAdapter({
- baseObject: myServerBaseObject,
- handleRequest(req) {/*...*/}
- })
+ const adapter = createServerAdapter(myServerBaseObject)
```

- `handleRequest` has been renamed to `handle` which has the same signature.

```diff
createServerAdapter({
- handleRequest(request) {
+ handle(request) {
})
```

- `Request` in the configuration needs to be passed as a second argument.

```diff
createServerAdapter({
- handleRequest(request) {
+ handle(request) {
- Request: MyRequestCtor
- })
+ }, MyRequestCtor)
```
51 changes: 25 additions & 26 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ Let's create a basic Hello World server adapter.
// myServerAdapter.ts
import { createServerAdapter } from '@whatwg-node/server'

export default createServerAdapter({
handleRequest(request: Request) {
return new Response(`Hello World!`, { status: 200 })
}
export default createServerAdapter((request: Request) => {
return new Response(`Hello World!`, { status: 200 })
})
```

Expand Down Expand Up @@ -203,25 +201,22 @@ For example, if you send a multipart request from a browser with `FormData`, you
```ts
import { createServerAdapter } from '@whatwg-node/server'

const myServerAdapter = createServerAdapter({
const myServerAdapter = createServerAdapter(async request => {
// Parse the request as `FormData`
const formData = await request.formData()
// Select the file
const file = formData.get('file')
// Process it as a string
const fileTextContent = await file.text()
// Select the other text parameter
const regularTextData = formData.get('additionalStuff')
// ...
async handleRequest(request) {
// Parse the request as `FormData`
const formData = await request.formData()
// Select the file
const file = formData.get('file')
// Process it as a string
const fileTextContent = await file.text()
// Select the other text parameter
const regularTextData = formData.get('additionalStuff')
// ...
return new Response('{ "message": "ok" }', {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
}
return new Response('{ "message": "ok" }', {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
})
```

Expand All @@ -236,6 +231,7 @@ We'd recommend to use [itty-router](https://github.com/kwhitley/itty-router) to
```ts
import { Router } from 'itty-router'
import { createServerAdapter } from '@whatwg-node/server'

// now let's create a router (note the lack of "new")
const router = Router()
// GET collection index
Expand All @@ -247,12 +243,15 @@ router.post('/todos', async request => {
const content = await request.json()
return new Response('Creating Todo: ' + JSON.stringify(content))
})

// Redirect to a URL
router.get('/google', () => Response.redirect('http://www.google.com'))

// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }))
// attach the router "handle" to our server adapter
const myServerAdapter = createServerAdapter({
handleRequest: router.handle
})

// attach the router to our server adapter
const myServerAdapter = createServerAdapter(router)

// Then use it in any environment
import { createServer } from 'http'
Expand Down
3 changes: 3 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
"@whatwg-node/fetch": "^0.4.0",
"tslib": "^2.3.1"
},
"devDependencies": {
"itty-router": "2.6.1"
},
"peerDependencies": {
"@types/node": "^18.0.6"
}
Expand Down
140 changes: 86 additions & 54 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,53 @@ import type { RequestListener, ServerResponse } from 'node:http';
import { isReadable, isServerResponse, NodeRequest, normalizeNodeRequest, sendNodeResponse } from './utils';
import { Request as PonyfillRequestCtor } from '@whatwg-node/fetch';

export interface CreateServerAdapterOptions<TServerContext, TBaseObject> {
/**
* WHATWG Fetch spec compliant `Request` constructor.
*/
Request?: typeof Request;
export interface ServerAdapterBaseObject<TServerContext, THandleRequest extends HandleRequestFn<TServerContext>> {
/**
* An async function that takes `Request` and the server context and returns a `Response`.
* If you use `requestListener`, the server context is `{ req: IncomingMessage, res: ServerResponse }`.
*/
handleRequest: (request: Request, serverContext: TServerContext) => Promise<Response>;
/**
* If you extend a server object with this, you can pass the original object and it will be extended with the required methods and functionalities.
*/
baseObject?: TBaseObject;
handle: THandleRequest;
}

export interface ServerAdapterObject<TServerContext> extends EventListenerObject {
export interface ServerAdapterObject<
TServerContext,
TBaseObject extends ServerAdapterBaseObject<TServerContext, HandleRequestFn<TServerContext>>
> extends EventListenerObject {
/**
* A basic request listener that takes a `Request` with the server context and returns a `Response`.
*/
handleRequest: (request: Request, serverContext: TServerContext) => Promise<Response>;
handleRequest: TBaseObject['handle'];
/**
* WHATWG Fetch spec compliant `fetch` function that can be used for testing purposes.
*/
fetch(request: Request, ...ctx: any[]): Promise<Response>;
fetch(urlStr: string, ...ctx: any[]): Promise<Response>;
fetch(urlStr: string, init: RequestInit, ...ctx: any[]): Promise<Response>;
fetch(url: URL, ...ctx: any[]): Promise<Response>;
fetch(url: URL, init: RequestInit, ...ctx: any[]): Promise<Response>;
fetch(request: Request, ...ctx: any[]): Promise<Response> | Response;
fetch(urlStr: string, ...ctx: any[]): Promise<Response> | Response;
fetch(urlStr: string, init: RequestInit, ...ctx: any[]): Promise<Response> | Response;
fetch(url: URL, ...ctx: any[]): Promise<Response> | Response;
fetch(url: URL, init: RequestInit, ...ctx: any[]): Promise<Response> | Response;

/**
* This function takes Node's request object and returns a WHATWG Fetch spec compliant `Response` object.
**/
handleNodeRequest(nodeRequest: NodeRequest, serverContext: TServerContext): Promise<Response>;
handleNodeRequest(nodeRequest: NodeRequest, serverContext: TServerContext): Promise<Response> | Response;
/**
* A request listener function that can be used with any Node server variation.
*/
requestListener: RequestListener;
/**
* Proxy to requestListener to mimic Node middlewares
*/
handle: RequestListener;
handle: RequestListener & ServerAdapterObject<TServerContext, TBaseObject>['fetch'];
}

export type ServerAdapter<TServerContext, TBaseObject> = TBaseObject &
export type ServerAdapter<
TServerContext,
THandleRequest extends HandleRequestFn<TServerContext>,
TBaseObject extends ServerAdapterBaseObject<TServerContext, THandleRequest>
> = Omit<TBaseObject, 'handle'> &
RequestListener &
ServerAdapterObject<TServerContext>['fetch'] &
ServerAdapterObject<TServerContext>;
ServerAdapterObject<TServerContext, TBaseObject>['fetch'] &
ServerAdapterObject<TServerContext, TBaseObject>;

async function handleWaitUntils(waitUntilPromises: Promise<unknown>[]) {
const waitUntils = await Promise.allSettled(waitUntilPromises);
Expand All @@ -62,26 +61,55 @@ async function handleWaitUntils(waitUntilPromises: Promise<unknown>[]) {
});
}

export function createServerAdapter<
TServerContext = {
req: NodeRequest;
res: ServerResponse;
waitUntil(promise: Promise<unknown>): void;
},
TBaseObject = unknown
>({
Request: RequestCtor = PonyfillRequestCtor,
handleRequest,
baseObject,
}: CreateServerAdapterOptions<TServerContext, TBaseObject>): ServerAdapter<TServerContext, TBaseObject> {
function fetchFn(input: RequestInfo | URL, init?: RequestInit, ...ctx: any[]): Promise<Response> {
type HandleRequestFn<TServerContext> = (
request: Request,
serverContext: TServerContext
) => Promise<Response> | Response;

type DefaultServerContext = {
req: NodeRequest;
res: ServerResponse;
waitUntil(promise: Promise<unknown>): void;
};

function createServerAdapter<
TServerContext = DefaultServerContext,
THandleRequest extends HandleRequestFn<TServerContext> = HandleRequestFn<TServerContext>
>(
serverAdapterBaseObject: THandleRequest
): ServerAdapter<TServerContext, THandleRequest, ServerAdapterBaseObject<TServerContext, THandleRequest>>;
function createServerAdapter<
TServerContext = DefaultServerContext,
THandleRequest extends HandleRequestFn<TServerContext> = HandleRequestFn<TServerContext>,
TBaseObject extends ServerAdapterBaseObject<TServerContext, THandleRequest> = ServerAdapterBaseObject<
TServerContext,
THandleRequest
>
>(serverAdapterBaseObject: TBaseObject): ServerAdapter<TServerContext, THandleRequest, TBaseObject>;
function createServerAdapter<
TServerContext = DefaultServerContext,
THandleRequest extends HandleRequestFn<TServerContext> = HandleRequestFn<TServerContext>,
TBaseObject extends ServerAdapterBaseObject<TServerContext, THandleRequest> = ServerAdapterBaseObject<
TServerContext,
THandleRequest
>
>(
serverAdapterBaseObject: TBaseObject | THandleRequest,
/**
* WHATWG Fetch spec compliant `Request` constructor.
*/
RequestCtor = PonyfillRequestCtor
): ServerAdapter<TServerContext, THandleRequest, TBaseObject> {
const handleRequest =
typeof serverAdapterBaseObject === 'function' ? serverAdapterBaseObject : serverAdapterBaseObject.handle;
function fetchFn(input: RequestInfo | URL, init?: RequestInit, ...ctx: any[]) {
if (typeof input === 'string' || input instanceof URL) {
return handleRequest(new RequestCtor(input, init), Object.assign({}, ...ctx));
}
return handleRequest(input, Object.assign({}, init, ...ctx));
}

function handleNodeRequest(nodeRequest: NodeRequest, serverContext: TServerContext): Promise<Response> {
function handleNodeRequest(nodeRequest: NodeRequest, serverContext: TServerContext) {
const request = normalizeNodeRequest(nodeRequest, RequestCtor);
return handleRequest(request, serverContext);
}
Expand Down Expand Up @@ -116,15 +144,6 @@ export function createServerAdapter<
event.respondWith(response$);
}

const adapterObj: ServerAdapterObject<TServerContext> = {
handleRequest,
fetch: fetchFn,
handleNodeRequest,
requestListener,
handleEvent,
handle: requestListener,
};

function genericRequestHandler(input: any, ctx: any, ...rest: any[]) {
if ('process' in globalThis && process.versions?.['bun'] != null) {
// This is required for bun
Expand Down Expand Up @@ -166,20 +185,25 @@ export function createServerAdapter<
return handleRequest(input, ctx);
}

const adapterObj: ServerAdapterObject<TServerContext, TBaseObject> = {
handleRequest,
fetch: fetchFn,
handleNodeRequest,
requestListener,
handleEvent,
handle: genericRequestHandler as any,
};

return new Proxy(genericRequestHandler as any, {
// It should have all the attributes of the handler function and the server instance
has: (_, prop) => {
return (baseObject && prop in baseObject) || prop in adapterObj || prop in genericRequestHandler;
return (
prop in adapterObj ||
prop in genericRequestHandler ||
(serverAdapterBaseObject && prop in serverAdapterBaseObject)
);
},
get: (_, prop) => {
if (baseObject) {
if (prop in baseObject) {
if (baseObject[prop].bind) {
return baseObject[prop].bind(baseObject);
}
return baseObject[prop];
}
}
if (adapterObj[prop]) {
if (adapterObj[prop].bind) {
return adapterObj[prop].bind(adapterObj);
Expand All @@ -192,9 +216,17 @@ export function createServerAdapter<
}
return genericRequestHandler[prop];
}
if (prop in serverAdapterBaseObject) {
if (serverAdapterBaseObject[prop].bind) {
return serverAdapterBaseObject[prop].bind(serverAdapterBaseObject);
}
return serverAdapterBaseObject[prop];
}
},
apply(_, __, [input, ctx]: Parameters<typeof genericRequestHandler>) {
return genericRequestHandler(input, ctx);
},
});
}

export { createServerAdapter };
Loading

0 comments on commit a67f447

Please sign in to comment.