@server/next 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +50 -0
- package/package.json +7 -7
- package/readme.md +16 -113
- package/src/helpers/define.js +2 -2
- package/src/helpers/handleRequest.js +4 -1
- package/src/index.js +33 -29
- package/src/index.test.js +23 -9
- package/src/pathPattern.js +1 -0
- package/src/url.test.js +24 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type Bucket = {};
|
|
2
|
+
|
|
3
|
+
type Method =
|
|
4
|
+
| "socket"
|
|
5
|
+
| "get"
|
|
6
|
+
| "head"
|
|
7
|
+
| "post"
|
|
8
|
+
| "put"
|
|
9
|
+
| "patch"
|
|
10
|
+
| "delete"
|
|
11
|
+
| "options";
|
|
12
|
+
|
|
13
|
+
type ServerOptions = {
|
|
14
|
+
port?: number;
|
|
15
|
+
views?: string | Bucket;
|
|
16
|
+
public?: string | Bucket;
|
|
17
|
+
uploads?: string | Bucket;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Context = {
|
|
21
|
+
method: Method;
|
|
22
|
+
headers: { [key: string]: string | string[] };
|
|
23
|
+
cookies: { [key: string]: any };
|
|
24
|
+
body?: any;
|
|
25
|
+
url: URL & { params: {}; query: {} };
|
|
26
|
+
options: ServerOptions;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Middleware = (ctx: Context) => any;
|
|
30
|
+
|
|
31
|
+
type Router = {};
|
|
32
|
+
|
|
33
|
+
declare interface Server {
|
|
34
|
+
(options?: ServerOptions): this;
|
|
35
|
+
|
|
36
|
+
socket(path: string, ...middleware: Middleware[]): this;
|
|
37
|
+
get(path: string, ...middleware: Middleware[]): this;
|
|
38
|
+
head(path: string, ...middleware: Middleware[]): this;
|
|
39
|
+
post(path: string, ...middleware: Middleware[]): this;
|
|
40
|
+
put(path: string, ...middleware: Middleware[]): this;
|
|
41
|
+
patch(path: string, ...middleware: Middleware[]): this;
|
|
42
|
+
del(path: string, ...middleware: Middleware[]): this;
|
|
43
|
+
options(path: string, ...middleware: Middleware[]): this;
|
|
44
|
+
|
|
45
|
+
use(...middleware: Middleware[]): this;
|
|
46
|
+
router(router: Router): this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare const server: Server;
|
|
50
|
+
export default server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "An experimental reimplementation of server.js focused on the DX",
|
|
5
5
|
"homepage": "https://node-server.com/",
|
|
6
6
|
"repository": "https://github.com/franciscop/server-next.git",
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"demo": "nodemon ./demo/app.js",
|
|
13
|
-
"build": "bun build src/index.js --outdir ./ --entry-naming server.js",
|
|
14
13
|
"start": "bun test --watch",
|
|
15
|
-
"
|
|
16
|
-
"test": "
|
|
14
|
+
"test": "bun test",
|
|
15
|
+
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
17
16
|
},
|
|
18
17
|
"keywords": [
|
|
19
18
|
"server",
|
|
@@ -21,9 +20,11 @@
|
|
|
21
20
|
"server.js"
|
|
22
21
|
],
|
|
23
22
|
"type": "module",
|
|
23
|
+
"types": "index.d.ts",
|
|
24
24
|
"main": "src/index.js",
|
|
25
25
|
"files": [
|
|
26
|
-
"src/"
|
|
26
|
+
"src/",
|
|
27
|
+
"index.d.ts"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=20.0.0"
|
|
@@ -31,8 +32,7 @@
|
|
|
31
32
|
"engineStrict": true,
|
|
32
33
|
"dependencies": {},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"jest": "^
|
|
35
|
-
"prettier": "^2.7.0"
|
|
35
|
+
"jest": "^29.7.0"
|
|
36
36
|
},
|
|
37
37
|
"jest": {
|
|
38
38
|
"testEnvironment": "jest-environment-node",
|
package/readme.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Server @ Next
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **⚠️ WIP** This is an **experimental library** right now!
|
|
4
|
+
|
|
5
|
+
[**Documentation Here**](https://node-server.com/documentation/)
|
|
4
6
|
|
|
5
7
|
A fully-fledged web server for Bun and Node.js, with all the basics covered for you:
|
|
6
8
|
|
|
@@ -9,26 +11,26 @@ import server from "@server/next";
|
|
|
9
11
|
|
|
10
12
|
// Create a running instance of the server
|
|
11
13
|
export default server(options)
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
.get("/books", () => Book.list())
|
|
15
|
+
.post("/books", { body: BookSchema }, (ctx) => {
|
|
16
|
+
return Book.create(ctx.body).save();
|
|
17
|
+
});
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
It includes all the things you would expect from a modern Server framework, like routing, static file serving,
|
|
19
|
-
|
|
20
|
-
> \* not yet available
|
|
20
|
+
It includes all the things you would expect from a modern Server framework, like routing, static file serving, body+file parsing, gzip+brotli, streaming, testing, error handling, websockets, etc. We also have integrations with these:
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
- KV Stores: in-memory, Redis, Consul, DynamoDB.
|
|
23
|
+
- Buckets: AWS S3, Cloudflare R2, Backblaze B2.
|
|
24
|
+
- Validation libraries: Zod, Joi, Yup.
|
|
23
25
|
|
|
24
26
|
```js
|
|
25
|
-
// index.test.js
|
|
27
|
+
// Easy testing as well - index.test.js
|
|
26
28
|
import app from "./";
|
|
27
29
|
|
|
28
30
|
it("can retrieve the homepage", async () => {
|
|
29
|
-
const res = await app.fetch(new Request("http://localhost:3000/
|
|
30
|
-
const
|
|
31
|
-
expect(
|
|
31
|
+
const res = await app.fetch(new Request("http://localhost:3000/books/"));
|
|
32
|
+
const books = await res.json();
|
|
33
|
+
expect(books[0]).toEqual({ id: 0, name: 'The Catcher In The Rye', ... });
|
|
32
34
|
});
|
|
33
35
|
```
|
|
34
36
|
|
|
@@ -43,18 +45,10 @@ Why? We live in the era of multi-cloud (Heroku, Workers, Lambda, etc) and multi-
|
|
|
43
45
|
- Changed the reply logic greatly, including the removal of `render()`. This is the main reason express was removed. Many servers don't need render() at all.
|
|
44
46
|
- **[security]** Removed mandatory CSRF token, since this is only useful for server-rendered pages and not for SPA. Now we provide an `auth` module instead.
|
|
45
47
|
|
|
46
|
-
Major changes:
|
|
47
|
-
|
|
48
|
-
- New fully fledged `ctx.url` that extends [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object inside `ctx` (also note: `ctx.url` is no longer a string):
|
|
49
|
-
- `ctx.params` is now `ctx.url.params`, e.g. `ctx.url.params.id`.
|
|
50
|
-
- `ctx.query` is now `ctx.url.query`, e.g. `ctx.url.query.search`.
|
|
51
|
-
- `ctx.path` is now `ctx.url.path` (or `ctx.url.pathname`).
|
|
52
|
-
- All URL properties are available, like `ctx.url.port`, `ctx.url.searchParams`, etc.
|
|
53
|
-
|
|
54
48
|
## Progress
|
|
55
49
|
|
|
56
50
|
- Router has all verbs, as well as URL pattern matches
|
|
57
|
-
- Full URL parsing, including `query` and `params` in ctx.url
|
|
51
|
+
- Full URL parsing, including `query` and `params` in `ctx.url`.
|
|
58
52
|
- Body and Files parsing is working (need testing)
|
|
59
53
|
- The middleware can return:
|
|
60
54
|
- A number and it'll be set as the status code
|
|
@@ -63,42 +57,6 @@ Major changes:
|
|
|
63
57
|
- An object with `status`, `body` and `headers` and it'll be set raw.
|
|
64
58
|
- Response compression works
|
|
65
59
|
|
|
66
|
-
## Some plugins
|
|
67
|
-
|
|
68
|
-
> This question is just some concepts/ideas
|
|
69
|
-
|
|
70
|
-
Plugins? Or internals?
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
import Bucket from 'bucket/s3';
|
|
74
|
-
import Redis from 'redis';
|
|
75
|
-
|
|
76
|
-
const bucket = Bucket('my-bucket', { id, key });
|
|
77
|
-
const cache = Redis('my-redis', ...);
|
|
78
|
-
|
|
79
|
-
const app = server({ public: bucket, uploads: bucket, cache });
|
|
80
|
-
|
|
81
|
-
app([
|
|
82
|
-
post('/uploads', ctx => {
|
|
83
|
-
// Without { public: bucket }, it'd be a local file in the filesystem
|
|
84
|
-
console.log(ctx.files.profile);
|
|
85
|
-
// file.name
|
|
86
|
-
// file.id
|
|
87
|
-
// file.path
|
|
88
|
-
// file.type
|
|
89
|
-
// file.size
|
|
90
|
-
|
|
91
|
-
// With { public: bucket }, it's the reference to the bucket file
|
|
92
|
-
console.log(ctx.files.profile);
|
|
93
|
-
// file.name
|
|
94
|
-
// file.id
|
|
95
|
-
// file.path
|
|
96
|
-
// file.type
|
|
97
|
-
// file.size
|
|
98
|
-
})
|
|
99
|
-
]);
|
|
100
|
-
```
|
|
101
|
-
|
|
102
60
|
## Examples
|
|
103
61
|
|
|
104
62
|
### Streams
|
|
@@ -156,58 +114,3 @@ export default server()
|
|
|
156
114
|
return 201;
|
|
157
115
|
});
|
|
158
116
|
```
|
|
159
|
-
|
|
160
|
-
```js
|
|
161
|
-
import server, { jwtAuth } from "server";
|
|
162
|
-
import BookRouter from "./BookRouter";
|
|
163
|
-
import UserRouter from "./UserRouter";
|
|
164
|
-
|
|
165
|
-
export default server({ port: 3000 })
|
|
166
|
-
.use(jwtAuth)
|
|
167
|
-
.route("/users", UserRouter)
|
|
168
|
-
.route("/books", BookRouter);
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
```js
|
|
172
|
-
// UserRouter.js -> A custom router
|
|
173
|
-
import server, { router } from "server";
|
|
174
|
-
|
|
175
|
-
export default router()
|
|
176
|
-
.get("/", getUsers)
|
|
177
|
-
.get("/:id", getUser)
|
|
178
|
-
.post("/", createUser);
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
MAYBE?
|
|
182
|
-
|
|
183
|
-
```js
|
|
184
|
-
// BookRouter.js -> a REST API router
|
|
185
|
-
import { RestRouter } from "server";
|
|
186
|
-
|
|
187
|
-
export default class BookRouter extends RestRouter {
|
|
188
|
-
async create(ctx) {
|
|
189
|
-
// POST /
|
|
190
|
-
}
|
|
191
|
-
async list(ctx) {
|
|
192
|
-
// GET /
|
|
193
|
-
}
|
|
194
|
-
async get(ctx) {
|
|
195
|
-
// GET /:id
|
|
196
|
-
}
|
|
197
|
-
async search(ctx) {
|
|
198
|
-
// GET /?...
|
|
199
|
-
}
|
|
200
|
-
async update(ctx) {
|
|
201
|
-
// PUT /:id
|
|
202
|
-
}
|
|
203
|
-
async set(ctx) {
|
|
204
|
-
// PATCH /:id
|
|
205
|
-
}
|
|
206
|
-
async delete(ctx) {
|
|
207
|
-
// DELETE /:id
|
|
208
|
-
}
|
|
209
|
-
async error(ctx) {
|
|
210
|
-
// ctx.error
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
```
|
package/src/helpers/define.js
CHANGED
|
@@ -4,7 +4,7 @@ import define from "./define.js";
|
|
|
4
4
|
import validate from "./validate.js";
|
|
5
5
|
|
|
6
6
|
export default async function handleRequest(handlers, ctx) {
|
|
7
|
-
for (let [matcher, ...cbs] of handlers[ctx.method]) {
|
|
7
|
+
for (let [method, matcher, ...cbs] of handlers[ctx.method]) {
|
|
8
8
|
const match = pathPattern(matcher, ctx.url.pathname || "/");
|
|
9
9
|
// Skip this whole middleware if there was no match
|
|
10
10
|
if (!match) continue;
|
|
@@ -22,6 +22,9 @@ export default async function handleRequest(handlers, ctx) {
|
|
|
22
22
|
return new Response(error.message, { status: error.status || 500 });
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
// When it's an HTTP method, break free after it's done (which will 404)
|
|
27
|
+
if (method !== "*") break;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
return new Response("Not Found", { status: 404 });
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,18 @@ export default function server(options = {}) {
|
|
|
19
19
|
|
|
20
20
|
this.platform = getMachine();
|
|
21
21
|
|
|
22
|
-
this.handlers = {
|
|
22
|
+
this.handlers = {
|
|
23
|
+
socket: [],
|
|
24
|
+
get: [],
|
|
25
|
+
head: [],
|
|
26
|
+
post: [],
|
|
27
|
+
put: [],
|
|
28
|
+
patch: [],
|
|
29
|
+
delete: [],
|
|
30
|
+
options: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
options.port = options.port || process.env.PORT || 3000;
|
|
23
34
|
|
|
24
35
|
options.views = options.views ? Bucket(options.views) : null;
|
|
25
36
|
options.public = options.public ? Bucket(options.public) : null;
|
|
@@ -57,14 +68,14 @@ export default function server(options = {}) {
|
|
|
57
68
|
}
|
|
58
69
|
response.end();
|
|
59
70
|
})
|
|
60
|
-
.listen(options.port
|
|
71
|
+
.listen(options.port);
|
|
61
72
|
})();
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
this.fetch = async (request, env, fetchCtx) => {
|
|
65
76
|
if (env?.upgrade(request)) return;
|
|
66
77
|
|
|
67
|
-
const ctx = await createWinterContext(request, options);
|
|
78
|
+
const ctx = await createWinterContext(request, options, this.platform);
|
|
68
79
|
ctx.platform = this.platform;
|
|
69
80
|
|
|
70
81
|
return await handleRequest(this.handlers, ctx);
|
|
@@ -72,62 +83,55 @@ export default function server(options = {}) {
|
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
// INTERNAL
|
|
75
|
-
server.prototype.handle = function (
|
|
76
|
-
if (
|
|
77
|
-
this.handlers
|
|
86
|
+
server.prototype.handle = function (method, path, ...middleware) {
|
|
87
|
+
if (method === "*") {
|
|
88
|
+
for (let m in this.handlers) {
|
|
89
|
+
this.handlers[m].push(["*", path, ...middleware]);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
this.handlers[method].push([method, path, ...middleware]);
|
|
78
93
|
}
|
|
79
|
-
|
|
94
|
+
|
|
80
95
|
return this;
|
|
81
96
|
};
|
|
82
97
|
|
|
83
98
|
server.prototype.socket = function (path, ...middleware) {
|
|
84
|
-
return this.handle("socket",
|
|
99
|
+
return this.handle("socket", path, ...middleware);
|
|
85
100
|
};
|
|
86
101
|
|
|
87
102
|
server.prototype.get = function (path, ...middleware) {
|
|
88
|
-
return this.handle("get",
|
|
103
|
+
return this.handle("get", path, ...middleware);
|
|
89
104
|
};
|
|
90
105
|
|
|
91
106
|
server.prototype.head = function (path, ...middleware) {
|
|
92
|
-
return this.handle("head",
|
|
107
|
+
return this.handle("head", path, ...middleware);
|
|
93
108
|
};
|
|
94
109
|
|
|
95
110
|
server.prototype.post = function (path, ...middleware) {
|
|
96
|
-
return this.handle("post",
|
|
111
|
+
return this.handle("post", path, ...middleware);
|
|
97
112
|
};
|
|
98
113
|
|
|
99
114
|
server.prototype.put = function (path, ...middleware) {
|
|
100
|
-
return this.handle("put",
|
|
115
|
+
return this.handle("put", path, ...middleware);
|
|
101
116
|
};
|
|
102
117
|
|
|
103
118
|
server.prototype.patch = function (path, ...middleware) {
|
|
104
|
-
return this.handle("patch",
|
|
119
|
+
return this.handle("patch", path, ...middleware);
|
|
105
120
|
};
|
|
106
121
|
|
|
107
122
|
server.prototype.del = function (path, ...middleware) {
|
|
108
|
-
return this.handle("del",
|
|
123
|
+
return this.handle("del", path, ...middleware);
|
|
109
124
|
};
|
|
110
125
|
|
|
111
126
|
server.prototype.options = function (path, ...middleware) {
|
|
112
|
-
return this.handle("options",
|
|
127
|
+
return this.handle("options", path, ...middleware);
|
|
113
128
|
};
|
|
114
129
|
|
|
115
130
|
server.prototype.use = function (...middleware) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
path = middleware.shift();
|
|
131
|
+
if (typeof middleware[0] === "string") {
|
|
132
|
+
return this.handle("*", ...middleware);
|
|
119
133
|
}
|
|
120
|
-
|
|
121
|
-
this.handle("socket", [path, ...middleware]);
|
|
122
|
-
this.handle("get", [path, ...middleware]);
|
|
123
|
-
this.handle("head", [path, ...middleware]);
|
|
124
|
-
this.handle("post", [path, ...middleware]);
|
|
125
|
-
this.handle("put", [path, ...middleware]);
|
|
126
|
-
this.handle("patch", [path, ...middleware]);
|
|
127
|
-
this.handle("del", [path, ...middleware]);
|
|
128
|
-
this.handle("options", [path, ...middleware]);
|
|
129
|
-
|
|
130
|
-
return this;
|
|
134
|
+
return this.handle("*", "*", ...middleware);
|
|
131
135
|
};
|
|
132
136
|
|
|
133
137
|
server.prototype.router = function (basePath, router) {
|
package/src/index.test.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import server, { status } from "./index.js";
|
|
2
2
|
|
|
3
|
-
describe("
|
|
3
|
+
describe("return different types", () => {
|
|
4
4
|
const app = server()
|
|
5
5
|
.get("/", () => "Hello world")
|
|
6
6
|
.get("/text", () => "Hello world")
|
|
7
7
|
.get("/array", () => ["Hello world"])
|
|
8
8
|
.get("/object", () => ({ hello: "world" }))
|
|
9
|
-
.get("/status", () => 201)
|
|
10
|
-
.post("/", (ctx) => status(201).send(ctx.body));
|
|
9
|
+
.get("/status", () => 201);
|
|
11
10
|
|
|
12
|
-
it("can
|
|
13
|
-
const res = await app.fetch(new Request("http://localhost:3000/"));
|
|
14
|
-
|
|
15
|
-
expect(res.status).toBe(200);
|
|
11
|
+
it("can get the plain text", async () => {
|
|
12
|
+
const res = await app.fetch(new Request("http://localhost:3000/text"));
|
|
16
13
|
expect(await res.text()).toBe("Hello world");
|
|
17
14
|
});
|
|
18
15
|
|
|
16
|
+
it("can get the array", async () => {
|
|
17
|
+
const res = await app.fetch(new Request("http://localhost:3000/array"));
|
|
18
|
+
expect(await res.json()).toEqual(["Hello world"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("can get the object", async () => {
|
|
22
|
+
const res = await app.fetch(new Request("http://localhost:3000/object"));
|
|
23
|
+
expect(await res.json()).toEqual({ hello: "world" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("can get the status", async () => {
|
|
27
|
+
const res = await app.fetch(new Request("http://localhost:3000/status"));
|
|
28
|
+
expect(res.status).toBe(201);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("simple post works", () => {
|
|
33
|
+
const app = server().post("/", (ctx) => status(201).send(ctx.body));
|
|
34
|
+
|
|
19
35
|
it("can post new data", async () => {
|
|
20
36
|
const res = await app.fetch(
|
|
21
37
|
new Request("http://localhost:3000/", {
|
|
@@ -41,5 +57,3 @@ describe("server", () => {
|
|
|
41
57
|
expect(await res.json()).toEqual({ hello: "world" });
|
|
42
58
|
});
|
|
43
59
|
});
|
|
44
|
-
|
|
45
|
-
describe("all the replies", () => {});
|
package/src/pathPattern.js
CHANGED
package/src/url.test.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import server from "./index.js";
|
|
2
|
+
|
|
3
|
+
describe("can match the url", () => {
|
|
4
|
+
it("stops at the first matching route", async () => {
|
|
5
|
+
const app = server()
|
|
6
|
+
.get("/:id", (ctx) => ctx.url.params)
|
|
7
|
+
.get("/*", (ctx) => ctx.url.params);
|
|
8
|
+
|
|
9
|
+
const res = await app.fetch(new Request("http://localhost:3000/hello"));
|
|
10
|
+
expect(await res.json()).toEqual({ id: "hello" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("but it doesn't if it's a use", async () => {
|
|
14
|
+
const app = server()
|
|
15
|
+
.use(() => {
|
|
16
|
+
// No-op
|
|
17
|
+
})
|
|
18
|
+
.get("/:id", (ctx) => ctx.url.params)
|
|
19
|
+
.get("/*", (ctx) => ctx.url.params);
|
|
20
|
+
|
|
21
|
+
const res = await app.fetch(new Request("http://localhost:3000/hello"));
|
|
22
|
+
expect(await res.json()).toEqual({ id: "hello" });
|
|
23
|
+
});
|
|
24
|
+
});
|