Skip to content

Commit c81504f

Browse files
authored
feat: add experimental support for Node.js (#479)
1 parent 76dca1c commit c81504f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1398
-339
lines changed

.github/workflows/oak-ci.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@ jobs:
2626
run: deno lint
2727

2828
- name: generate bundle
29-
run: deno bundle mod.ts oak.bundle.js
29+
run: deno bundle --import-map import-map.json mod.ts oak.bundle.js
3030

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

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

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

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

4344
- name: generate lcov
4445
if: matrix.os == 'ubuntu-latest'

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
!.vscode
22
oak.bundle.js
33
cov.lcov
4-
cov/
4+
cov/
5+
npm/

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"deno.enable": true,
33
"deno.unstable": true,
44
"deno.lint": true,
5+
"deno.importMap": "./import-map.json",
56
"deno.codeLens.testArgs": [
67
"--allow-net",
78
"--allow-read",

README.md

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
![Custom badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Fupdates%2Fx%2Foak%2Fmod.ts)
99
[![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)
1010

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

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

28-
> ⚠️ _Warning_ The examples in this README pull from `main`, which may not make
29-
> sense to do when you are looking to actually deploy a workload. You would want
30-
> to "pin" to a particular version which is compatible with the version of Deno
31-
> you are using and has a fixed set of APIs you would expect.
32-
> `https://deno.land/x/` supports using git tags in the URL to direct you at a
33-
> particular version. So to use version 3.0.0 of oak, you would want to import
34-
> `https://deno.land/x/[email protected]/mod.ts`.
29+
> ⚠️ _Warning_ The examples in this README pull from `main` and are designed for
30+
> Deno CLI or Deno Deploy, which may not make sense to do when you are looking
31+
> to actually deploy a workload. You would want to "pin" to a particular version
32+
> which is compatible with the version of Deno you are using and has a fixed set
33+
> of APIs you would expect. `https://deno.land/x/` supports using git tags in
34+
> the URL to direct you at a particular version. So to use version 3.0.0 of oak,
35+
> you would want to import `https://deno.land/x/[email protected]/mod.ts`.
3536
3637
## Application, middleware, and context
3738

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

852+
## Node.js
853+
854+
As of oak v10.3, oak is experimentally supported on Node.js 16.5 and later. The
855+
package is available on npm as `@oakserver/oak`. The package exports are the
856+
same as the exports of the `mod.ts` when using under Deno and the package
857+
auto-detects it is running under Node.js.
858+
859+
A basic example:
860+
861+
**main.mjs**
862+
863+
```js
864+
import { Application } from "@oakserver/oak";
865+
866+
const app = new Application();
867+
868+
app.use((ctx) => {
869+
ctx.response.body = "Hello from oak under Node.js";
870+
});
871+
872+
app.listen({ port: 8000 });
873+
```
874+
875+
There are a few notes about the support:
876+
877+
- The package is only available as an ESM distribution. This is because there
878+
are a couple places where the framework takes advantage of top level await,
879+
which can only be supported in ES modules under Node.js.
880+
- Currently only HTTP/1.1 support is available. There are plans to add HTTP/2.
881+
- Web Socket upgrades are not currently supported. This is planned for the
882+
future. Trying to upgrade to a web socket will cause an error to be thrown.
883+
851884
---
852885

853886
There are several modules that are directly adapted from other modules. They

_build_npm.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --allow-run
2+
// Copyright 2018-2022 the oak authors. All rights reserved. MIT license.
3+
4+
/**
5+
* This is the build script for building the oak framework into a Node.js
6+
* compatible npm package.
7+
*
8+
* @module
9+
*/
10+
11+
import { build, emptyDir } from "https://deno.land/x/[email protected]/mod.ts";
12+
import { copy } from "https://deno.land/[email protected]/fs/copy.ts";
13+
14+
async function start() {
15+
await emptyDir("./npm");
16+
await copy("fixtures", "npm/esm/fixtures", { overwrite: true });
17+
18+
await build({
19+
entryPoints: ["./mod.ts"],
20+
outDir: "./npm",
21+
shims: {
22+
blob: true,
23+
crypto: true,
24+
deno: true,
25+
undici: true,
26+
custom: [{
27+
package: {
28+
name: "stream/web",
29+
},
30+
globalNames: ["ReadableStream", "TransformStream"],
31+
}],
32+
},
33+
scriptModule: false,
34+
test: true,
35+
compilerOptions: {
36+
importHelpers: true,
37+
target: "ES2021",
38+
},
39+
package: {
40+
name: "@oakserver/oak",
41+
version: Deno.args[0],
42+
description: "A middleware framework for handling HTTP requests",
43+
license: "MIT",
44+
engines: {
45+
node: ">=16.5.0 <18",
46+
},
47+
repository: {
48+
type: "git",
49+
url: "git+https://github.com/oakserver/oak.git",
50+
},
51+
bugs: {
52+
url: "https://github.com/oakserver/oak/issues",
53+
},
54+
dependencies: {
55+
"tslib": "~2.3.1",
56+
},
57+
devDependencies: {
58+
"@types/node": "^16",
59+
},
60+
},
61+
});
62+
63+
await Deno.copyFile("LICENSE", "npm/LICENSE");
64+
await Deno.copyFile("README.md", "npm/README.md");
65+
}
66+
67+
start();

application.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import { HttpServerNative, NativeRequest } from "./http_server_native.ts";
66
import { KeyStack } from "./keyStack.ts";
77
import { compose, Middleware } from "./middleware.ts";
88
import { cloneState } from "./structured_clone.ts";
9-
import { Key, Server, ServerConstructor } from "./types.d.ts";
10-
import { assert, isConn } from "./util.ts";
9+
import {
10+
Key,
11+
Listener,
12+
Server,
13+
ServerConstructor,
14+
ServerRequest,
15+
} from "./types.d.ts";
16+
import { assert, isConn, isNode } from "./util.ts";
1117

1218
export interface ListenOptionsBase extends Deno.ListenOptions {
1319
secure?: false;
@@ -69,7 +75,7 @@ interface ApplicationListenEventListenerObject {
6975

7076
interface ApplicationListenEventInit extends EventInit {
7177
hostname: string;
72-
listener: Deno.Listener;
78+
listener: Listener;
7379
port: number;
7480
secure: boolean;
7581
serverType: "native" | "custom";
@@ -81,7 +87,7 @@ type ApplicationListenEventListenerOrEventListenerObject =
8187

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

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

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

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

153+
const DEFAULT_SERVER: ServerConstructor<ServerRequest> = isNode()
154+
? (await import("./http_server_node.ts")).HttpServerNode
155+
: HttpServerNative;
156+
// deno-lint-ignore no-explicit-any
157+
const LocalErrorEvent: typeof ErrorEvent = (globalThis as any).ErrorEvent ??
158+
(await import("./node_shims.ts")).ErrorEvent;
159+
147160
export class ApplicationErrorEvent<S extends AS, AS extends State>
148-
extends ErrorEvent {
161+
extends LocalErrorEvent {
149162
context?: Context<S, AS>;
150163

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

191204
export class ApplicationListenEvent extends Event {
192205
hostname: string;
193-
listener: Deno.Listener;
206+
listener: Listener;
194207
port: number;
195208
secure: boolean;
196209
serverType: "native" | "custom";
@@ -239,7 +252,7 @@ export class Application<AS extends State = Record<string, any>>
239252
#contextState: "clone" | "prototype" | "alias" | "empty";
240253
#keys?: KeyStack;
241254
#middleware: Middleware<State, Context<State, AS>>[] = [];
242-
#serverConstructor: ServerConstructor<NativeRequest>;
255+
#serverConstructor: ServerConstructor<ServerRequest>;
243256

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

281-
constructor(options: ApplicationOptions<AS> = {}) {
294+
constructor(options: ApplicationOptions<AS, ServerRequest> = {}) {
282295
super();
283296
const {
284297
state,
285298
keys,
286299
proxy,
287-
serverConstructor = HttpServerNative,
300+
serverConstructor = DEFAULT_SERVER,
288301
contextState = "clone",
289302
logErrors = true,
290303
} = options;
@@ -354,7 +367,7 @@ export class Application<AS extends State = Record<string, any>>
354367

355368
/** Processing registered middleware on each request. */
356369
async #handleRequest(
357-
request: NativeRequest,
370+
request: ServerRequest,
358371
secure: boolean,
359372
state: RequestState,
360373
): Promise<void> {
@@ -581,4 +594,26 @@ export class Application<AS extends State = Record<string, any>>
581594
inspect({ "#middleware": this.#middleware, keys, proxy, state })
582595
}`;
583596
}
597+
598+
[Symbol.for("nodejs.util.inspect.custom")](
599+
depth: number,
600+
// deno-lint-ignore no-explicit-any
601+
options: any,
602+
inspect: (value: unknown, options?: unknown) => string,
603+
) {
604+
if (depth < 0) {
605+
return options.stylize(`[${this.constructor.name}]`, "special");
606+
}
607+
608+
const newOptions = Object.assign({}, options, {
609+
depth: options.depth === null ? null : options.depth - 1,
610+
});
611+
const { keys, proxy, state } = this;
612+
return `${options.stylize(this.constructor.name, "special")} ${
613+
inspect(
614+
{ "#middleware": this.#middleware, keys, proxy, state },
615+
newOptions,
616+
)
617+
}`;
618+
}
584619
}

application_test.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ import { Status } from "./deps.ts";
2121
import { HttpServerNative, NativeRequest } from "./http_server_native.ts";
2222
import { httpErrors } from "./httpError.ts";
2323
import { KeyStack } from "./keyStack.ts";
24-
import type { Data, Server, ServerConstructor } from "./types.d.ts";
24+
import type {
25+
Data,
26+
Listener,
27+
Server,
28+
ServerConstructor,
29+
ServerRequest,
30+
} from "./types.d.ts";
31+
import { isNode } from "./util.ts";
2532

2633
const { test } = Deno;
2734

@@ -56,7 +63,7 @@ function setup(
5663

5764
return [
5865
class MockNativeServer<AS extends State = Record<string, any>>
59-
implements Server<NativeRequest> {
66+
implements Server<ServerRequest> {
6067
constructor(
6168
_app: Application<AS>,
6269
private options: Deno.ListenOptions | Deno.ListenTlsOptions,
@@ -68,14 +75,14 @@ function setup(
6875
serverClosed = true;
6976
}
7077

71-
listen(): Deno.Listener {
78+
listen(): Listener {
7279
return {
7380
addr: {
7481
transport: "tcp",
75-
hostname: this.options.hostname,
82+
hostname: this.options.hostname ?? "localhost",
7683
port: this.options.port,
7784
},
78-
} as Deno.Listener;
85+
} as Listener;
7986
}
8087

8188
async *[Symbol.asyncIterator]() {
@@ -901,7 +908,9 @@ test({
901908
fn() {
902909
assertEquals(
903910
Deno.inspect(new Application()),
904-
`Application { "#middleware": [], keys: undefined, proxy: false, state: {} }`,
911+
isNode()
912+
? `Application { '#middleware': [], keys: undefined, proxy: false, state: {} }`
913+
: `Application { "#middleware": [], keys: undefined, proxy: false, state: {} }`,
905914
);
906915
teardown();
907916
},

0 commit comments

Comments
 (0)