@server/next 0.12.1 → 0.15.0
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/package.json +29 -51
- package/readme.md +66 -67
- package/src/RequestLogger.js +77 -0
- package/src/ServerUrl.js +32 -0
- package/src/ServerUrl.test.js +45 -0
- package/src/color.js +26 -0
- package/src/index.js +280 -0
- package/src/parseBody.js +96 -0
- package/src/parseBody.test.js +69 -0
- package/src/pathPattern.js +24 -0
- package/src/pathPattern.test.js +72 -0
- package/index.js +0 -515
package/package.json
CHANGED
|
@@ -1,65 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "An experimental
|
|
5
|
-
"
|
|
6
|
-
"
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "An experimental reimplementation of server.js focused on the DX",
|
|
5
|
+
"homepage": "https://node-server.com/",
|
|
6
|
+
"repository": "https://github.com/franciscop/server-next.git",
|
|
7
|
+
"bugs": "https://github.com/franciscop/server-next/issues",
|
|
8
|
+
"funding": "https://www.paypal.me/franciscopresencia/19",
|
|
9
|
+
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
|
+
"license": "UNLICENSED",
|
|
7
11
|
"scripts": {
|
|
8
|
-
"
|
|
9
|
-
"start": "npm run test -- --watch",
|
|
12
|
+
"start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
10
13
|
"size": "echo \"$(gzip -c index.js | wc -c) bytes\" # Only for Unix",
|
|
11
|
-
"test": "
|
|
12
|
-
},
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=12.0.0"
|
|
14
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
15
15
|
},
|
|
16
|
-
"engineStrict": true,
|
|
17
16
|
"keywords": [
|
|
18
17
|
"server",
|
|
19
18
|
"node",
|
|
20
19
|
"server.js"
|
|
21
20
|
],
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"dependencies": {},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@babel/core": "^7.4.4",
|
|
32
|
-
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
|
33
|
-
"@babel/preset-env": "^7.4.4",
|
|
34
|
-
"jest": "^24.8.0",
|
|
35
|
-
"rollup": "^1.21.4"
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "src/index.js",
|
|
23
|
+
"files": [
|
|
24
|
+
"src/"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=14.0.0"
|
|
36
28
|
},
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"targets": {
|
|
43
|
-
"node": "current"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
],
|
|
48
|
-
"plugins": [
|
|
49
|
-
[
|
|
50
|
-
"@babel/plugin-syntax-dynamic-import"
|
|
51
|
-
]
|
|
52
|
-
],
|
|
53
|
-
"env": {
|
|
54
|
-
"test": {
|
|
55
|
-
"plugins": [
|
|
56
|
-
"dynamic-import-node"
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
}
|
|
29
|
+
"engineStrict": true,
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"dotenv": "^16.0.1",
|
|
32
|
+
"spinnies": "^0.5.1",
|
|
33
|
+
"urlpattern-polyfill": "^5.0.0"
|
|
60
34
|
},
|
|
61
|
-
"
|
|
62
|
-
"
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"jest": "^26.0.1",
|
|
37
|
+
"prettier": "^2.7.0"
|
|
63
38
|
},
|
|
64
|
-
"
|
|
39
|
+
"jest": {
|
|
40
|
+
"testEnvironment": "jest-environment-node",
|
|
41
|
+
"transform": {}
|
|
42
|
+
}
|
|
65
43
|
}
|
package/readme.md
CHANGED
|
@@ -1,87 +1,86 @@
|
|
|
1
|
+
# Server @ Next
|
|
2
|
+
|
|
1
3
|
> **VERY EARLY WORK IN PROGRESS**
|
|
2
4
|
>
|
|
3
5
|
> **I don't know what will come out of this, if anything! Treat as the most experimental thing you've ever seen**
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
New implementation from scratch. This version has these changes when compared with the 1.0 (WIP):
|
|
8
|
-
|
|
9
|
-
- Tiny footprint with no dependencies, all bundled in a single file. Installing and using the full library takes under 10kb (target limit).
|
|
10
|
-
- Faster! Reimplemented from scratch for speed. With raw ES7 and a tiny code footprint, your server will fly.
|
|
11
|
-
- Modern ES6 syntax for both the library and examples.
|
|
12
|
-
- Error handling improved greatly.
|
|
13
|
-
- Compatible with Cloudflare Workers so you can run the same code on Node.js or on a Worker.
|
|
14
|
-
- Not using express underneath anymore. Considering keeping the compatibility layer anynway (since Express itself is a thin layer).
|
|
15
|
-
- 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.
|
|
16
|
-
- **[security]** Removed mandatory CSRF token, since this is only useful for server-rendered pages and not for SPA. You can activate it with a single option as before.
|
|
17
|
-
|
|
18
|
-
Conceptually, the world is moving out of server-rendered websites so we are as well. Now instead we treat APIs as first-class citizens.
|
|
19
|
-
|
|
20
|
-
```js
|
|
21
|
-
// It will be published under `server` in the future
|
|
22
|
-
import server, { get, post } from "@server/next";
|
|
23
|
-
|
|
24
|
-
server(
|
|
25
|
-
get("/", () => `Homepage works! Try '/users/abc'`),
|
|
26
|
-
get("/users/:id", ({ params }) => `The user is ${params.id}`),
|
|
27
|
-
post("/users", () => ({ id: "abc", name: "Francisco" }))
|
|
28
|
-
);
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Demo
|
|
32
|
-
|
|
33
|
-
Create a directory, initialize npm and install `@server/next`:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
mkdir server-demo && cd server-demo
|
|
37
|
-
npm init --yes
|
|
38
|
-
npm install @server/next
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Create a `index.js` with a home route and a path route:
|
|
7
|
+
A fully-fledged web server for Node.js, with all the basics covered for you:
|
|
42
8
|
|
|
43
9
|
```js
|
|
44
|
-
import server, { get } from
|
|
45
|
-
|
|
46
|
-
server
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
10
|
+
import server, { get, post, put, use, error } from 'server';
|
|
11
|
+
|
|
12
|
+
// Create a running instance of the server
|
|
13
|
+
const app = server(config, [pluginA, pluginB]);
|
|
14
|
+
|
|
15
|
+
// Attach handlers to the instance
|
|
16
|
+
app([
|
|
17
|
+
get('/users', getUsers),
|
|
18
|
+
post('/users', createUser),
|
|
19
|
+
put('/users/:id', editUser),
|
|
20
|
+
use('/admin/*', dashboard),
|
|
21
|
+
error(ctx => console.log(ctx.error))
|
|
22
|
+
]);
|
|
50
23
|
```
|
|
51
24
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{
|
|
56
|
-
"main": "index.js",
|
|
57
|
-
"type": "module",
|
|
58
|
-
...
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Start it with `node .` and visit http://localhost:3000/ 🎉
|
|
63
|
-
|
|
25
|
+
It includes all the things you would expect from a modern Server framework, like routing, static file serving, body+file parsing, gzip+brotli\*, streaming, etc.
|
|
64
26
|
|
|
27
|
+
> \* not yet available
|
|
65
28
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
If you also want to build it (for example, for Cloudflare Workers) you can build it with `rollup`. Please see more info [in Cloudflare's official documentation](https://developers.cloudflare.com/workers/archive/writing-workers/using-npm-modules/).
|
|
29
|
+
## Upgradingn server
|
|
69
30
|
|
|
31
|
+
Why? The ecosystem is moving out of server-rendered websites so we are as well. Now instead we treat APIs as first-class citizens. Desired improvements (WIP!):
|
|
70
32
|
|
|
33
|
+
- Tiny footprint with no dependencies, all bundled in a single file. Installing and using the full library takes under 10kb (target limit).
|
|
34
|
+
- Faster! Reimplemented from scratch for speed. With raw ES6+ and a tiny code footprint, your server will fly.
|
|
35
|
+
- Modern ES6+ESM syntax for both the library and examples.
|
|
36
|
+
- Error handling improved greatly.
|
|
37
|
+
- Not using express underneath anymore. Considering keeping the compatibility layer anyway (since Express itself is a thin layer).
|
|
38
|
+
- 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.
|
|
39
|
+
- **[security]** Removed mandatory CSRF token, since this is only useful for server-rendered pages and not for SPA. You can activate it with a single option as before.
|
|
71
40
|
|
|
72
|
-
## TODO
|
|
73
41
|
|
|
74
|
-
|
|
42
|
+
## Progress
|
|
75
43
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
44
|
+
- Router has `get` and `post`, as well as URL pattern matches
|
|
45
|
+
- The middleware can return:
|
|
46
|
+
- A number and it'll be set as the status code
|
|
47
|
+
- A string and it'll be sent as plain text or html (if it starts with "<")
|
|
48
|
+
- A readStream and it'll be piped to the response
|
|
49
|
+
- An object with `status`, `body` and `headers` and it'll be set raw.
|
|
79
50
|
|
|
80
51
|
|
|
52
|
+
## Some plugins
|
|
81
53
|
|
|
82
|
-
|
|
54
|
+
> This question is just some concepts/ideas
|
|
83
55
|
|
|
84
|
-
|
|
56
|
+
Plugins? Or internals?
|
|
85
57
|
|
|
86
|
-
|
|
87
|
-
|
|
58
|
+
```js
|
|
59
|
+
import Bucket from 'bucket/s3';
|
|
60
|
+
import Redis from 'redis';
|
|
61
|
+
|
|
62
|
+
const bucket = Bucket('my-bucket', { id, key });
|
|
63
|
+
const cache = Redis('my-redis', ...);
|
|
64
|
+
|
|
65
|
+
const app = server({ public: bucket, cache });
|
|
66
|
+
|
|
67
|
+
app([
|
|
68
|
+
post('/uploads', ctx => {
|
|
69
|
+
// Without { public: bucket }, it'd be a local file in the filesystem
|
|
70
|
+
console.log(ctx.files.profile);
|
|
71
|
+
// file.name
|
|
72
|
+
// file.id
|
|
73
|
+
// file.path
|
|
74
|
+
// file.type
|
|
75
|
+
// file.size
|
|
76
|
+
|
|
77
|
+
// With { public: bucket }, it's the reference to the bucket file
|
|
78
|
+
console.log(ctx.files.profile);
|
|
79
|
+
// file.name
|
|
80
|
+
// file.id
|
|
81
|
+
// file.path
|
|
82
|
+
// file.type
|
|
83
|
+
// file.size
|
|
84
|
+
})
|
|
85
|
+
]);
|
|
86
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import Spinnies from "spinnies";
|
|
2
|
+
import color from "./color.js";
|
|
3
|
+
|
|
4
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
5
|
+
|
|
6
|
+
const format = (n, [below, above], limit = Infinity) => {
|
|
7
|
+
if (n < 1000) {
|
|
8
|
+
return n.toFixed(0).padStart(4, " ") + ` {dim}${below.padEnd(2, " ")}{/}`;
|
|
9
|
+
}
|
|
10
|
+
const clean = (n / 1000).toFixed(n > 90000 ? 0 : 1).padStart(4, " ");
|
|
11
|
+
if (n > 10 * limit) {
|
|
12
|
+
return `{red}${clean} ${above.padEnd(2, " ")}{/}`;
|
|
13
|
+
} else if (n > limit) {
|
|
14
|
+
return `{yellow}${clean} ${above.padEnd(2, " ")}{/}`;
|
|
15
|
+
} else {
|
|
16
|
+
return `${clean} {dim}${above.padEnd(2, " ")}{/}`;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const findSize = ({ headers, body, size }, res) => {
|
|
21
|
+
if (size) return size;
|
|
22
|
+
if (body) return body.length;
|
|
23
|
+
if (headers["content-length"]) return +headers["content-length"];
|
|
24
|
+
return 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function simpleType(type) {
|
|
28
|
+
const simpler = {
|
|
29
|
+
"text/html": "html",
|
|
30
|
+
"text/plain": "text",
|
|
31
|
+
"image/svg+xml": "svg",
|
|
32
|
+
"image/png": "png",
|
|
33
|
+
"text/css": "css",
|
|
34
|
+
"application/javascript": "js",
|
|
35
|
+
"application/json": "json",
|
|
36
|
+
"text/markdown": "md",
|
|
37
|
+
};
|
|
38
|
+
return simpler[type] || type;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let spinnies;
|
|
42
|
+
export default function RequestLogger(ctx) {
|
|
43
|
+
this.id = Math.round(Math.random() * 100000);
|
|
44
|
+
|
|
45
|
+
if (!isProduction) {
|
|
46
|
+
if (!spinnies) {
|
|
47
|
+
spinnies = new Spinnies({ succeedColor: "white", failColor: "white" });
|
|
48
|
+
}
|
|
49
|
+
const method = ("[" + ctx.method.toLowerCase() + "]").padEnd(6, " ");
|
|
50
|
+
const text = color(`{dim}${method}{/} ${ctx.url?.path || ctx.url}`);
|
|
51
|
+
spinnies.add(`spinner-${this.id}`, { text });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.end = function (ctx) {
|
|
55
|
+
if (!isProduction) {
|
|
56
|
+
const status = ctx.res.status;
|
|
57
|
+
|
|
58
|
+
const paddedPath = `${ctx.url?.path || ctx.url} {dim}`.padEnd(30, "╌");
|
|
59
|
+
const statColor =
|
|
60
|
+
status < 300 ? "green" : status < 500 ? "yellow" : "red";
|
|
61
|
+
const statusBlock = `{${statColor}}[${ctx.res.status}]{/}`;
|
|
62
|
+
const resSize = format(findSize(ctx.res), ["b", "kb"], 100000);
|
|
63
|
+
const t = Math.round(ctx.time._total - ctx.time._init);
|
|
64
|
+
const resTime = format(t, ["ms", "s"], 1000);
|
|
65
|
+
|
|
66
|
+
const type = simpleType(
|
|
67
|
+
ctx.res.type || ctx.res.headers["content-type"] || "----"
|
|
68
|
+
);
|
|
69
|
+
const method = ("[" + ctx.method + "]").padEnd(6, " ");
|
|
70
|
+
const reqText = `{dim}${method}{/} ${paddedPath}`;
|
|
71
|
+
const resText = `${statusBlock} ${resSize} ${resTime} ${type}`;
|
|
72
|
+
const text = color(`${reqText}╌›{/} ${resText}`);
|
|
73
|
+
|
|
74
|
+
spinnies.succeed(`spinner-${this.id}`, { text });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/ServerUrl.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default class ServerUrl {
|
|
2
|
+
constructor(urlString) {
|
|
3
|
+
const url = new URL(urlString);
|
|
4
|
+
this.href = url.href;
|
|
5
|
+
this.origin = url.origin;
|
|
6
|
+
this.protocol = url.protocol;
|
|
7
|
+
this.username = url.username;
|
|
8
|
+
this.password = url.password;
|
|
9
|
+
this.host = url.host;
|
|
10
|
+
this.hostname = url.hostname;
|
|
11
|
+
this.port = url.port ? +url.port : null; // make it an intege
|
|
12
|
+
this.pathname = url.pathname;
|
|
13
|
+
this.path = url.pathname; // nicknam
|
|
14
|
+
this.params = {}; // The URL parameter
|
|
15
|
+
this.search = url.search;
|
|
16
|
+
this.searchParams = url.searchParams;
|
|
17
|
+
this.query = this.getQuery(url.searchParams.entries()); // As a plain object
|
|
18
|
+
this.hash = url.hash;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getQuery(entries) {
|
|
22
|
+
const query = {};
|
|
23
|
+
for (const [key, value] of entries) {
|
|
24
|
+
query[key] = value;
|
|
25
|
+
}
|
|
26
|
+
return query;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toString() {
|
|
30
|
+
return this.href;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import ServerUrl from "./ServerUrl.js";
|
|
2
|
+
|
|
3
|
+
describe("getUrl()", () => {
|
|
4
|
+
it("can parse a basic URL", () => {
|
|
5
|
+
const url = new ServerUrl("https://example.com/");
|
|
6
|
+
|
|
7
|
+
// Extended values
|
|
8
|
+
expect(url.path).toEqual("/");
|
|
9
|
+
expect(url.query).toEqual({});
|
|
10
|
+
|
|
11
|
+
// Base values
|
|
12
|
+
expect(url.href).toBe("https://example.com/");
|
|
13
|
+
expect(url.pathname).toEqual("/");
|
|
14
|
+
expect(url.protocol).toBe("https:");
|
|
15
|
+
expect(url.username).toBe("");
|
|
16
|
+
expect(url.password).toBe("");
|
|
17
|
+
expect(url.host).toBe("example.com");
|
|
18
|
+
expect(url.hostname).toBe("example.com");
|
|
19
|
+
expect(url.port).toBe(null);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("can parse localhost", () => {
|
|
23
|
+
const url = new ServerUrl("http://localhost:3000/");
|
|
24
|
+
|
|
25
|
+
// Extended values
|
|
26
|
+
expect(url.path).toEqual("/");
|
|
27
|
+
expect(url.query).toEqual({});
|
|
28
|
+
expect(url.params).toEqual({});
|
|
29
|
+
|
|
30
|
+
// Base values
|
|
31
|
+
expect(url.href).toBe("http://localhost:3000/");
|
|
32
|
+
expect(url.pathname).toEqual("/");
|
|
33
|
+
expect(url.protocol).toBe("http:");
|
|
34
|
+
expect(url.username).toBe("");
|
|
35
|
+
expect(url.password).toBe("");
|
|
36
|
+
expect(url.host).toBe("localhost:3000");
|
|
37
|
+
expect(url.hostname).toBe("localhost");
|
|
38
|
+
expect(url.port).toBe(3000);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("can be stringified", () => {
|
|
42
|
+
const url = new ServerUrl("http://localhost:3000/");
|
|
43
|
+
expect(url + "").toBe("http://localhost:3000/");
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/color.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Add color to a string: color('hello {bright}world{/bright}')
|
|
2
|
+
// or a template literal: color`hello {bright}world{/bright}`
|
|
3
|
+
// Supports NO_COLOR, multiple styles, and closing with "{/}"
|
|
4
|
+
// prettier-ignore
|
|
5
|
+
const map = {
|
|
6
|
+
reset: 0, bright: 1, dim: 2, under: 4, blink: 5, reverse: 7,
|
|
7
|
+
|
|
8
|
+
black: 30, red: 31, green: 32, yellow: 33,
|
|
9
|
+
blue: 34, magenta: 35, cyan: 36, white: 37,
|
|
10
|
+
|
|
11
|
+
bgblack: 40, bgred: 41, bggreen: 42, bgyellow: 43,
|
|
12
|
+
bgblue: 44, bgmagenta: 45, bgcyan: 46, bgwhite: 47,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const replace = (k) => (process.env.NO_COLOR ? "" : `\x1b[${map[k]}m`);
|
|
16
|
+
|
|
17
|
+
export default function color(str, ...vals) {
|
|
18
|
+
if (typeof str === "string") {
|
|
19
|
+
return str
|
|
20
|
+
.replaceAll(/\{(\w+)\}/g, (m, k) => replace(k))
|
|
21
|
+
.replaceAll(/\{\/\w*\}/g, replace("reset"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// For template literals, put them together first and then color it
|
|
25
|
+
return color(str[0] + vals.map((v, i) => v + str[i + 1]).join(""));
|
|
26
|
+
}
|