Skip to content

Commit

Permalink
feat: add experimental support for Node.js (#479)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk authored Feb 21, 2022
1 parent 76dca1c commit c81504f
Show file tree
Hide file tree
Showing 49 changed files with 1,398 additions and 339 deletions.
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/[email protected]/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/[email protected]/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/[email protected]/mod.ts";
import { copy } from "https://deno.land/[email protected]/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

0 comments on commit c81504f

Please sign in to comment.