Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add experimental support for Node.js #479

Merged
merged 10 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/oak-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ jobs:
run: deno lint

- name: generate bundle
run: deno bundle mod.ts oak.bundle.js
run: deno bundle --import-map import-map.json mod.ts oak.bundle.js

- name: run tests
run: deno test --allow-read --allow-write --allow-net --jobs 4
run: deno test --import-map import-map.json --allow-read --allow-write --allow-net --jobs 4 --ignore=npm

- name: run tests no check
run: deno test --allow-read --allow-write --allow-net --no-check --jobs 4
run: deno test --import-map import-map.json --allow-read --allow-write --allow-net --no-check --jobs 4 --ignore=npm

- name: run tests unstable
run: deno test --coverage=./cov --allow-read --allow-write --allow-net --unstable --jobs 4
run: deno test --coverage=./cov --import-map import-map.json --allow-read --allow-write --allow-net --unstable --jobs 4 --ignore=npm

- name: run tests using dom libs
run: deno test --unstable --allow-read --allow-write --allow-net --config dom.tsconfig.json --jobs 4
- name: test build for Node.js
if: matrix.os == 'ubuntu-latest'
run: deno run --allow-read --allow-write --allow-net --allow-env --allow-run _build_npm.ts

- name: generate lcov
if: matrix.os == 'ubuntu-latest'
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
!.vscode
oak.bundle.js
cov.lcov
cov/
cov/
npm/
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"deno.enable": true,
"deno.unstable": true,
"deno.lint": true,
"deno.importMap": "./import-map.json",
"deno.codeLens.testArgs": [
"--allow-net",
"--allow-read",
Expand Down
51 changes: 42 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
![Custom badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Fupdates%2Fx%2Foak%2Fmod.ts)
[![Custom badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Flatest-version%2Fx%2Foak%2Fmod.ts)](https://doc.deno.land/https/deno.land/x/oak/mod.ts)

A middleware framework for Deno's native HTTP server and
[Deno Deploy](https://deno.com/deploy). It also includes a middleware router.
A middleware framework for Deno's native HTTP server,
[Deno Deploy](https://deno.com/deploy) and Node.js 16.5 and later. It also
includes a middleware router.

This middleware framework is inspired by [Koa](https://github.com/koajs/koa/)
and middleware router inspired by
Expand All @@ -25,13 +26,13 @@ Also, check out our [FAQs](https://oakserver.github.io/oak/FAQ) and the
[awesome-oak](https://oakserver.github.io/awesome-oak/) site of community
resources.

> ⚠️ _Warning_ The examples in this README pull from `main`, which may not make
> sense to do when you are looking to actually deploy a workload. You would want
> to "pin" to a particular version which is compatible with the version of Deno
> you are using and has a fixed set of APIs you would expect.
> `https://deno.land/x/` supports using git tags in the URL to direct you at a
> particular version. So to use version 3.0.0 of oak, you would want to import
> `https://deno.land/x/oak@v3.0.0/mod.ts`.
> ⚠️ _Warning_ The examples in this README pull from `main` and are designed for
> Deno CLI or Deno Deploy, which may not make sense to do when you are looking
> to actually deploy a workload. You would want to "pin" to a particular version
> which is compatible with the version of Deno you are using and has a fixed set
> of APIs you would expect. `https://deno.land/x/` supports using git tags in
> the URL to direct you at a particular version. So to use version 3.0.0 of oak,
> you would want to import `https://deno.land/x/oak@v3.0.0/mod.ts`.

## Application, middleware, and context

Expand Down Expand Up @@ -848,6 +849,38 @@ testing oak middleware you might create. See the
[Testing with oak](https://oakserver.github.io/oak/testing) for more
information.

## Node.js

As of oak v10.3, oak is experimentally supported on Node.js 16.5 and later. The
package is available on npm as `@oakserver/oak`. The package exports are the
same as the exports of the `mod.ts` when using under Deno and the package
auto-detects it is running under Node.js.

A basic example:

**main.mjs**

```js
import { Application } from "@oakserver/oak";

const app = new Application();

app.use((ctx) => {
ctx.response.body = "Hello from oak under Node.js";
});

app.listen({ port: 8000 });
```

There are a few notes about the support:

- The package is only available as an ESM distribution. This is because there
are a couple places where the framework takes advantage of top level await,
which can only be supported in ES modules under Node.js.
- Currently only HTTP/1.1 support is available. There are plans to add HTTP/2.
- Web Socket upgrades are not currently supported. This is planned for the
future. Trying to upgrade to a web socket will cause an error to be thrown.

---

There are several modules that are directly adapted from other modules. They
Expand Down
67 changes: 67 additions & 0 deletions _build_npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --allow-run
// Copyright 2018-2022 the oak authors. All rights reserved. MIT license.

/**
* This is the build script for building the oak framework into a Node.js
* compatible npm package.
*
* @module
*/

import { build, emptyDir } from "https://deno.land/x/dnt@0.19.0/mod.ts";
import { copy } from "https://deno.land/std@0.126.0/fs/copy.ts";

async function start() {
await emptyDir("./npm");
await copy("fixtures", "npm/esm/fixtures", { overwrite: true });

await build({
entryPoints: ["./mod.ts"],
outDir: "./npm",
shims: {
blob: true,
crypto: true,
deno: true,
undici: true,
custom: [{
package: {
name: "stream/web",
},
globalNames: ["ReadableStream", "TransformStream"],
}],
},
scriptModule: false,
test: true,
compilerOptions: {
importHelpers: true,
target: "ES2021",
},
package: {
name: "@oakserver/oak",
version: Deno.args[0],
description: "A middleware framework for handling HTTP requests",
license: "MIT",
engines: {
node: ">=16.5.0 <18",
},
repository: {
type: "git",
url: "git+https://github.com/oakserver/oak.git",
},
bugs: {
url: "https://github.com/oakserver/oak/issues",
},
dependencies: {
"tslib": "~2.3.1",
},
devDependencies: {
"@types/node": "^16",
},
},
});

await Deno.copyFile("LICENSE", "npm/LICENSE");
await Deno.copyFile("README.md", "npm/README.md");
}

start();
59 changes: 47 additions & 12 deletions application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import { HttpServerNative, NativeRequest } from "./http_server_native.ts";
import { KeyStack } from "./keyStack.ts";
import { compose, Middleware } from "./middleware.ts";
import { cloneState } from "./structured_clone.ts";
import { Key, Server, ServerConstructor } from "./types.d.ts";
import { assert, isConn } from "./util.ts";
import {
Key,
Listener,
Server,
ServerConstructor,
ServerRequest,
} from "./types.d.ts";
import { assert, isConn, isNode } from "./util.ts";

export interface ListenOptionsBase extends Deno.ListenOptions {
secure?: false;
Expand Down Expand Up @@ -69,7 +75,7 @@ interface ApplicationListenEventListenerObject {

interface ApplicationListenEventInit extends EventInit {
hostname: string;
listener: Deno.Listener;
listener: Listener;
port: number;
secure: boolean;
serverType: "native" | "custom";
Expand All @@ -81,7 +87,7 @@ type ApplicationListenEventListenerOrEventListenerObject =

/** Available options that are used when creating a new instance of
* {@linkcode Application}. */
export interface ApplicationOptions<S> {
export interface ApplicationOptions<S, R extends ServerRequest> {
/** Determine how when creating a new context, the state from the application
* should be applied. A value of `"clone"` will set the state as a clone of
* the app state. Any non-cloneable or non-enumerable properties will not be
Expand Down Expand Up @@ -124,7 +130,7 @@ export interface ApplicationOptions<S> {
* requests.
*
* Generally this is only used for testing. */
serverConstructor?: ServerConstructor<NativeRequest>;
serverConstructor?: ServerConstructor<R>;

/** The initial state object for the application, of which the type can be
* used to infer the type of the state for both the application and any of the
Expand All @@ -136,16 +142,23 @@ interface RequestState {
handling: Set<Promise<void>>;
closing: boolean;
closed: boolean;
server: Server<NativeRequest>;
server: Server<ServerRequest>;
}

// deno-lint-ignore no-explicit-any
export type State = Record<string | number | symbol, any>;

const ADDR_REGEXP = /^\[?([^\]]*)\]?:([0-9]{1,5})$/;

const DEFAULT_SERVER: ServerConstructor<ServerRequest> = isNode()
? (await import("./http_server_node.ts")).HttpServerNode
: HttpServerNative;
// deno-lint-ignore no-explicit-any
const LocalErrorEvent: typeof ErrorEvent = (globalThis as any).ErrorEvent ??
(await import("./node_shims.ts")).ErrorEvent;

export class ApplicationErrorEvent<S extends AS, AS extends State>
extends ErrorEvent {
extends LocalErrorEvent {
context?: Context<S, AS>;

constructor(eventInitDict: ApplicationErrorEventInit<S, AS>) {
Expand Down Expand Up @@ -190,7 +203,7 @@ function logErrorListener<S extends AS, AS extends State>(

export class ApplicationListenEvent extends Event {
hostname: string;
listener: Deno.Listener;
listener: Listener;
port: number;
secure: boolean;
serverType: "native" | "custom";
Expand Down Expand Up @@ -239,7 +252,7 @@ export class Application<AS extends State = Record<string, any>>
#contextState: "clone" | "prototype" | "alias" | "empty";
#keys?: KeyStack;
#middleware: Middleware<State, Context<State, AS>>[] = [];
#serverConstructor: ServerConstructor<NativeRequest>;
#serverConstructor: ServerConstructor<ServerRequest>;

/** A set of keys, or an instance of `KeyStack` which will be used to sign
* cookies read and set by the application to avoid tampering with the
Expand Down Expand Up @@ -278,13 +291,13 @@ export class Application<AS extends State = Record<string, any>>
*/
state: AS;

constructor(options: ApplicationOptions<AS> = {}) {
constructor(options: ApplicationOptions<AS, ServerRequest> = {}) {
super();
const {
state,
keys,
proxy,
serverConstructor = HttpServerNative,
serverConstructor = DEFAULT_SERVER,
contextState = "clone",
logErrors = true,
} = options;
Expand Down Expand Up @@ -354,7 +367,7 @@ export class Application<AS extends State = Record<string, any>>

/** Processing registered middleware on each request. */
async #handleRequest(
request: NativeRequest,
request: ServerRequest,
secure: boolean,
state: RequestState,
): Promise<void> {
Expand Down Expand Up @@ -581,4 +594,26 @@ export class Application<AS extends State = Record<string, any>>
inspect({ "#middleware": this.#middleware, keys, proxy, state })
}`;
}

[Symbol.for("nodejs.util.inspect.custom")](
depth: number,
// deno-lint-ignore no-explicit-any
options: any,
inspect: (value: unknown, options?: unknown) => string,
) {
if (depth < 0) {
return options.stylize(`[${this.constructor.name}]`, "special");
}

const newOptions = Object.assign({}, options, {
depth: options.depth === null ? null : options.depth - 1,
});
const { keys, proxy, state } = this;
return `${options.stylize(this.constructor.name, "special")} ${
inspect(
{ "#middleware": this.#middleware, keys, proxy, state },
newOptions,
)
}`;
}
}
21 changes: 15 additions & 6 deletions application_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ import { Status } from "./deps.ts";
import { HttpServerNative, NativeRequest } from "./http_server_native.ts";
import { httpErrors } from "./httpError.ts";
import { KeyStack } from "./keyStack.ts";
import type { Data, Server, ServerConstructor } from "./types.d.ts";
import type {
Data,
Listener,
Server,
ServerConstructor,
ServerRequest,
} from "./types.d.ts";
import { isNode } from "./util.ts";

const { test } = Deno;

Expand Down Expand Up @@ -56,7 +63,7 @@ function setup(

return [
class MockNativeServer<AS extends State = Record<string, any>>
implements Server<NativeRequest> {
implements Server<ServerRequest> {
constructor(
_app: Application<AS>,
private options: Deno.ListenOptions | Deno.ListenTlsOptions,
Expand All @@ -68,14 +75,14 @@ function setup(
serverClosed = true;
}

listen(): Deno.Listener {
listen(): Listener {
return {
addr: {
transport: "tcp",
hostname: this.options.hostname,
hostname: this.options.hostname ?? "localhost",
port: this.options.port,
},
} as Deno.Listener;
} as Listener;
}

async *[Symbol.asyncIterator]() {
Expand Down Expand Up @@ -901,7 +908,9 @@ test({
fn() {
assertEquals(
Deno.inspect(new Application()),
`Application { "#middleware": [], keys: undefined, proxy: false, state: {} }`,
isNode()
? `Application { '#middleware': [], keys: undefined, proxy: false, state: {} }`
: `Application { "#middleware": [], keys: undefined, proxy: false, state: {} }`,
);
teardown();
},
Expand Down
Loading