Skip to content

Commit

Permalink
feat(node-utils): introduces @edge-runtime/node-utils to run edge com…
Browse files Browse the repository at this point in the history
…pliant code in Node.js environment (#219)

chore(node-utils): introduces edge-related utilities
chore(primitives): improves `fetch` type
chore: only considers src/ folder when running coverage with jest
chore(ponyfill): ignores types for fetch parameters
doc(node-utils): adds README and documentation page

Co-authored-by: Javi Velasco <javier.velasco86@gmail.com>
Co-authored-by: Kiko Beats <josefrancisco.verdu@gmail.com>
  • Loading branch information
3 people authored Dec 19, 2022
1 parent f80789b commit eef6b34
Show file tree
Hide file tree
Showing 29 changed files with 1,535 additions and 203 deletions.
6 changes: 6 additions & 0 deletions .changeset/silly-bikes-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@edge-runtime/node-utils': patch
'@edge-runtime/primitives': patch
---

Introducing @edge-runtime/node-utils to run edge-compliant code in Node.js environment
1 change: 1 addition & 0 deletions docs/pages/packages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"format": "@edge-runtime/format",
"jest-environment": "@edge-runtime/jest-environment",
"jest-expect": "@edge-runtime/jest-expect",
"node-utils": "@edge-runtime/node-utils",
"ponyfill": "@edge-runtime/ponyfill",
"primitives": "@edge-runtime/primitives",
"runtime": "edge-runtime",
Expand Down
140 changes: 140 additions & 0 deletions docs/pages/packages/node-utils.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Callout } from 'nextra-theme-docs'
import { Tabs, Tab } from '../../components/tabs'

# Edge Runtime Node Utils

The **@edge-runtime/node-utils** package contains utilities to run web compliant code into a Node.js environment.

<Callout type='warning' emoji='⚠️'>
Please note this is an alpha version.
</Callout>

## Installation

<Tabs items={['npm', 'yarn', 'pnpm']} storageKey="selected-pkg-manager">
<Tab>
```sh
npm install @edge-runtime/node-utils --save
```
</Tab>
<Tab>
```sh
yarn add @edge-runtime/node-utils
```
</Tab>
<Tab>
```sh
pnpm install @edge-runtime/node-utils --save
```
</Tab>
</Tabs>

This package includes built-in TypeScript support.

## Usage

```ts
import { once } = from 'node:events'
import { createServer } from 'node:http'
import { buildToNodeHandler } from '@edge-runtime/node-utils'

// 1. builds a transformer, using Node.js@18 globals, and a base url for URL constructor.
const transformToNode = buildToNodeHandler(global, {
origin: 'http://example.com',
})

const server = await createServer(
// 2. takes an web compliant request handler, that uses Web globals like Request and Response,
// and turn it into a Node.js compliant request handler.
transformToNode(async (req: Request) => new Response(req.body))
)

// 3. start the node.js server
server.listen()
await once(server, 'listening')

// 4. invoke the request handler
const response = await fetch(
`http://localhost:${(server.address() as AddressInfo).port}`,
{ method: 'POST', body: 'hello world' }
)

console.log(await response.text()) // is 'hello world'
await server.close()
```

## API

### buildToNodeHandler(dependencies, options): toNodeHandler(handler: WebHandler): NodeHandler

Builds a transformer function to turn an web compliant request handler (`(req: Request) => Promise<Response> | Response | null | undefined`) into
a Node.js compliant [request handler](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) (`(req: IncomingMessage, res: ServerResponse) => Promise<void> | void`).

**Limitation:** it does support the web handler second parameter, so `waitUntil` is not implemented yet.

#### dependencies: object

List of Web globals used by the transformer function:

- [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
- [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
- [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
- [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)

When running in Node.js 18+ (or Node.js 16 with [--experimental-fetch](https://nodejs.org/docs/latest-v16.x/api/cli.html#node_optionsoptions)), you may pass the ones from `global` scope.

```js
import { buildToNodeHandler } from '@edge-runtime/node-utils'

buildToNodeHandler(globals, {
/* ... options */
})
```

Otherwise, you can reuse primitives from [@edge-runtime/primitives](/packages/primitives)

```js
import { buildToNodeHandler } from '@edge-runtime/node-utils'
import * as primitives from '@edge-runtime/primitives'

buildToNodeHandler(primitives, {
/* ... options */
})
```

#### options: object

Options used to build the transformer function, including:

##### origin: string

Origin used to turn the incoming request relative url into a full [Request.url](https://developer.mozilla.org/en-US/docs/Web/API/Request/url) string.

### buildToRequest(dependencies, options): toRequest(request: IncomingMessage, options: object): Request

Builds a transformer function to turn a Node.js [IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage) into a Web [Request].
It needs globals Web contstructor a [dependencies](#dependencies-object), as well as [options](#options-object) to handle incoming url.

### toOutgoingHeaders(headers: Headers): OutgoingHttpHeaders

Turns Web [Request.headers](https://developer.mozilla.org/en-US/docs/Web/API/Request/headers) into
Node.js `ServerResponse` [OutgoingHttpHeaders](https://nodejs.org/api/http.html#responsegetheaders).

Includes `set-cookie` special handling, spliting multiple values when relevant.

### buildToHeaders(dependencies): toHeaders(nodeHeaders: IncomingHttpHeaders): Headers

Builds a transformer to turn Node.js [IncomingHttpHeaders](https://nodejs.org/api/http.html#messageheaders) into
Web [Request.headers](https://developer.mozilla.org/en-US/docs/Web/API/Request/headers).

### toToReadable(webStream: ReadableStream, options: object): Readable

Turns Web [ReadableStream](https://nodejs.org/api/stream.html#readable-streams)
(typically, the [Response.body](https://developer.mozilla.org/en-US/docs/Web/API/Response/body)) into
Node.js [Readable](https://nodejs.org/api/stream.html#readable-streams) stream.

### buildToReadableStream(dependencies): toReadableStream(stream: Readable): ReadableStream

Builds a transformer to turn Node.js [Readable](https://nodejs.org/api/stream.html#readable-streams)
(typically, the [IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage)'s payload) into
Web [ReadableStream](https://nodejs.org/api/stream.html#readable-streams).
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export default (rootDir: string): Config.InitialOptions => {
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
}
}
38 changes: 38 additions & 0 deletions packages/node-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<div align="center">
<br>
<img src="https://edge-runtime.vercel.app/og-image.png" alt="edge-runtime logo">
<br>
<br>
<p align="center"><strong>@edge-runtime/node-utils</strong>: Utilities to run web compliant code into a Node.js environment.</p>
<p align="center">See <a href="https://edge-runtime.vercel.app/packages/node-utils" target='_blank' rel='noopener noreferrer'>@edge-runtime/node-utils</a> section in our <a href="https://edge-runtime.vercel.app/" target='_blank' rel='noopener noreferrer'>website</a> for more information.</p>
<br>
</div>

## Install

**Note: this is an alpha version.**

Using npm:

```sh
npm install @edge-runtime/node-utils --save
```

or using yarn:

```sh
yarn add @edge-runtime/node-utils --dev
```

or using pnpm:

```sh
pnpm install @edge-runtime/node-utils --save
```

## License

**@edge-runtime/node-utils** © [Vercel](https://vercel.com), released under the [MPLv2](https://github.com/vercel/edge-runtime/blob/main/LICENSE.md) License.<br>
Authored and maintained by [Vercel](https://vercel.com) with help from [contributors](https://github.com/vercel/edge-runtime/contributors).

> [vercel.com](https://vercel.com) · GitHub [Vercel](https://github.com/vercel) · Twitter [@vercel](https://twitter.com/vercel)
5 changes: 5 additions & 0 deletions packages/node-utils/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import buildConfig from '../../jest.config'
import type { Config } from '@jest/types'

const config: Config.InitialOptions = buildConfig(__dirname)
export default config
38 changes: 38 additions & 0 deletions packages/node-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@edge-runtime/node-utils",
"description": "Helpers for running edge-compliant code in Node.js environment",
"homepage": "https://edge-runtime.vercel.app/packages/node-utils",
"version": "1.0.0-alpha.1",
"main": "dist/index.js",
"module": "dist/index.mjs",
"repository": {
"directory": "packages/node-utils",
"type": "git",
"url": "git+https://github.com/vercel/edge-runtime.git"
},
"bugs": {
"url": "https://github.com/vercel/edge-runtime/issues"
},
"keywords": [],
"devDependencies": {
"@edge-runtime/primitives": "workspace:2.0.2",
"@types/test-listen": "1.1.0",
"test-listen": "1.1.0",
"tsup": "6"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"clean:build": "rm -rf dist",
"clean:node": "rm -rf node_modules",
"prebuild": "pnpm run clean:build",
"test": "jest"
},
"license": "MPLv2",
"publishConfig": {
"access": "public"
},
"types": "dist/index.d.ts"
}
52 changes: 52 additions & 0 deletions packages/node-utils/src/edge-to-node/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { IncomingMessage, ServerResponse } from 'node:http'
import type {
WebHandler,
NodeHandler,
BuildDependencies,
RequestOptions,
} from '../types'
import { buildToRequest } from '../node-to-edge/request'
import { mergeIntoServerResponse, toOutgoingHeaders } from './headers'
import { toToReadable } from './stream'

export function buildToNodeHandler(
dependencies: BuildDependencies,
options: RequestOptions
) {
const toRequest = buildToRequest(dependencies)
return function toNodeHandler(webHandler: WebHandler): NodeHandler {
return (request: IncomingMessage, response: ServerResponse) => {
const maybePromise = webHandler(toRequest(request, options))
if (maybePromise instanceof Promise) {
maybePromise.then((webResponse) =>
toServerResponse(webResponse, response)
)
} else {
toServerResponse(maybePromise, response)
}
}
}
}

function toServerResponse(
webResponse: Response | null | undefined,
serverResponse: ServerResponse
) {
if (!webResponse) {
serverResponse.end()
return
}
mergeIntoServerResponse(
// @ts-ignore getAll() is not standard https://fetch.spec.whatwg.org/#headers-class
toOutgoingHeaders(webResponse.headers),
serverResponse
)

serverResponse.statusCode = webResponse.status
serverResponse.statusMessage = webResponse.statusText
if (!webResponse.body) {
serverResponse.end()
return
}
toToReadable(webResponse.body).pipe(serverResponse)
}
Loading

1 comment on commit eef6b34

@vercel
Copy link

@vercel vercel bot commented on eef6b34 Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

edge-runtime – ./

edge-runtime.vercel.app
edge-runtime.vercel.sh
edge-runtime-git-main.vercel.sh

Please sign in to comment.