@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 CHANGED
@@ -1,65 +1,43 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.12.1",
4
- "description": "An experimental Server.js reimplementation",
5
- "main": "index.js",
6
- "type": "module",
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
- "build": "rollup src/index.js --output.format es --name server --output.file index.js && npm run size",
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": "jest --detectOpenHandles"
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
- "files": [],
23
- "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
24
- "license": "UNLICENSED",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/franciscop/server-next.git"
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
- "babel": {
38
- "presets": [
39
- [
40
- "@babel/preset-env",
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
- "bugs": {
62
- "url": "https://github.com/franciscop/server-next/issues"
35
+ "devDependencies": {
36
+ "jest": "^26.0.1",
37
+ "prettier": "^2.7.0"
63
38
  },
64
- "homepage": "https://github.com/franciscop/server-next#readme"
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
- # Server.js
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 "@server/next";
45
-
46
- server(
47
- get("/", ctx => `Hello there!`),
48
- get("/:path", ctx => `Visited "${ctx.params.path}"`)
49
- ).then(ctx => console.log(`Running on ${ctx.runtime}`));
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
- Modify `package.json` to add `"type": "module"` for that nice `import` syntax:
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
- ## Building for Cloudflare
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
- New guides coming:
42
+ ## Progress
75
43
 
76
- - Modify all documentation to use the new ES6 syntax. Explain how to bundle and run it. Maybe create `npx @server/build`.
77
- - Consider other serverless architectures.
78
- - How to even test? Create a test suite that makes requests.
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
- ## Others
54
+ > This question is just some concepts/ideas
83
55
 
84
- Random thoughts and ramblings:
56
+ Plugins? Or internals?
85
57
 
86
- - Consider normalizing a `kv` store sorf-of, which would be very useful for both sessions and user-code. Right now we are using/supporting Redis.
87
- - Startup time is critical for serverless.
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
+ }
@@ -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
+ }