@server/next 0.15.0 → 0.16.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
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",
package/readme.md CHANGED
@@ -7,7 +7,7 @@
7
7
  A fully-fledged web server for Node.js, with all the basics covered for you:
8
8
 
9
9
  ```js
10
- import server, { get, post, put, use, error } from 'server';
10
+ import server, { get, post, put, use } from 'server';
11
11
 
12
12
  // Create a running instance of the server
13
13
  const app = server(config, [pluginA, pluginB]);
@@ -17,12 +17,11 @@ app([
17
17
  get('/users', getUsers),
18
18
  post('/users', createUser),
19
19
  put('/users/:id', editUser),
20
- use('/admin/*', dashboard),
21
- error(ctx => console.log(ctx.error))
20
+ use('/admin/*', dashboard)
22
21
  ]);
23
22
  ```
24
23
 
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.
24
+ It includes all the things you would expect from a modern Server framework, like routing, static file serving, body+file parsing, gzip+brotli, streaming, server-timing, plugins\*, etc.
26
25
 
27
26
  > \* not yet available
28
27
 
@@ -41,7 +40,9 @@ Why? The ecosystem is moving out of server-rendered websites so we are as well.
41
40
 
42
41
  ## Progress
43
42
 
44
- - Router has `get` and `post`, as well as URL pattern matches
43
+ - Router has all verbs, as well as URL pattern matches
44
+ - Full URL parsing, including `query` and `params` in ctx.url.
45
+ - Body and Files parsing is working (TODO: try with binary files and add tests)
45
46
  - The middleware can return:
46
47
  - A number and it'll be set as the status code
47
48
  - A string and it'll be sent as plain text or html (if it starts with "<")
@@ -17,13 +17,6 @@ const format = (n, [below, above], limit = Infinity) => {
17
17
  }
18
18
  };
19
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
20
  function simpleType(type) {
28
21
  const simpler = {
29
22
  "text/html": "html",
@@ -59,7 +52,7 @@ export default function RequestLogger(ctx) {
59
52
  const statColor =
60
53
  status < 300 ? "green" : status < 500 ? "yellow" : "red";
61
54
  const statusBlock = `{${statColor}}[${ctx.res.status}]{/}`;
62
- const resSize = format(findSize(ctx.res), ["b", "kb"], 100000);
55
+ const resSize = format(ctx.res.size || 0, ["b", "kb"], 100000);
63
56
  const t = Math.round(ctx.time._total - ctx.time._init);
64
57
  const resTime = format(t, ["ms", "s"], 1000);
65
58
 
package/src/ServerUrl.js CHANGED
@@ -1,21 +1,57 @@
1
- export default class ServerUrl {
1
+ import { inspect } from "util";
2
+
3
+ const colors = {
4
+ string: process.env.NO_COLOR ? "" : "\x1b[32m",
5
+ number: process.env.NO_COLOR ? "" : "\x1b[33m",
6
+ };
7
+
8
+ const properties = [
9
+ "hash",
10
+ "host",
11
+ "hostname",
12
+ "href",
13
+ "origin",
14
+ "params",
15
+ "password",
16
+ "path",
17
+ "pathname",
18
+ "port",
19
+ "protocol",
20
+ "query",
21
+ "search",
22
+ "searchParams",
23
+ "username",
24
+ ];
25
+
26
+ export default class ServerUrl extends URL {
2
27
  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;
28
+ super(urlString);
29
+
30
+ const custom = {
31
+ port: +this.port || null,
32
+ path: this.pathname,
33
+ params: {},
34
+ query: this.getQuery(this.searchParams.entries()),
35
+ };
36
+
37
+ for (let key of properties) {
38
+ const value = key in custom ? custom[key] : this[key];
39
+ Object.defineProperty(this, key, {
40
+ value,
41
+ enumerable: true,
42
+ writable: true,
43
+ });
44
+ }
45
+ }
46
+
47
+ [inspect.custom]() {
48
+ const props = Object.keys(this)
49
+ .map((key) => {
50
+ const color = colors[typeof this[key]] || "";
51
+ return ` ${key}: ${color}${inspect(this[key])}\x1b[0m`;
52
+ })
53
+ .join("\n");
54
+ return `ServerUrl {\n${props}\n}`;
19
55
  }
20
56
 
21
57
  getQuery(entries) {
@@ -17,6 +17,24 @@ describe("getUrl()", () => {
17
17
  expect(url.host).toBe("example.com");
18
18
  expect(url.hostname).toBe("example.com");
19
19
  expect(url.port).toBe(null);
20
+
21
+ expect(Object.keys(url)).toEqual([
22
+ "hash",
23
+ "host",
24
+ "hostname",
25
+ "href",
26
+ "origin",
27
+ "params",
28
+ "password",
29
+ "path",
30
+ "pathname",
31
+ "port",
32
+ "protocol",
33
+ "query",
34
+ "search",
35
+ "searchParams",
36
+ "username",
37
+ ]);
20
38
  });
21
39
 
22
40
  it("can parse localhost", () => {
package/src/index.js CHANGED
@@ -2,7 +2,9 @@ import http from "node:http";
2
2
  import fs from "node:fs";
3
3
  import fsp from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import zlib from "node:zlib";
5
6
  import { pipeline } from "node:stream/promises";
7
+ import { Readable, PassThrough } from "node:stream";
6
8
 
7
9
  import "dotenv/config";
8
10
 
@@ -29,6 +31,35 @@ const exists = (file) => {
29
31
  );
30
32
  };
31
33
 
34
+ const measure = (ctx) => {
35
+ const sizeUp = new PassThrough();
36
+ ctx.res.size = 0;
37
+ sizeUp.on("data", (chunk) => {
38
+ ctx.res.size += chunk.length;
39
+ });
40
+ return sizeUp;
41
+ };
42
+
43
+ const findEncoding = (acceptEncoding) => {
44
+ let encoding;
45
+ if (/\bbr\b/.test(acceptEncoding)) {
46
+ encoding = "br";
47
+ } else if (/\bgzip\b/.test(acceptEncoding)) {
48
+ encoding = "gzip";
49
+ } else if (/\bdeflate\b/.test(acceptEncoding)) {
50
+ encoding = "deflate";
51
+ }
52
+
53
+ const methods = {
54
+ deflate: () => zlib.createDeflate(),
55
+ gzip: () => zlib.createGzip(),
56
+ br: () => zlib.createBrotliCompress(),
57
+ };
58
+
59
+ const compress = methods[encoding];
60
+ return [encoding, compress];
61
+ };
62
+
32
63
  const api = {
33
64
  get: [],
34
65
  post: [],
@@ -119,7 +150,6 @@ export default function (options = {}, plugins) {
119
150
  ctx.res.body = out;
120
151
  const isHtml = out.trim().startsWith("<");
121
152
  ctx.res.headers["content-type"] = isHtml ? "text/html" : "text/plain";
122
- ctx.res.headers["content-length"] = Buffer.byteLength(out);
123
153
  } else {
124
154
  if (out.pipe) {
125
155
  ctx.res.body = out;
@@ -127,7 +157,6 @@ export default function (options = {}, plugins) {
127
157
  } else {
128
158
  // Plain object
129
159
  if (out.type) ctx.res.headers["content-type"] = out.type;
130
- if (out.length) ctx.res.headers["content-length"] = out.length;
131
160
  ctx.res.headers = { ...ctx.res.headers, ...(out.headers || {}) };
132
161
  ctx.res.body = out.body || "";
133
162
  ctx.res.status = out.status || 200;
@@ -147,21 +176,30 @@ export default function (options = {}, plugins) {
147
176
  ctx.res.headers["server-timing"] += ", ";
148
177
  }
149
178
  });
179
+
180
+ const [encoding, compress] = findEncoding(ctx.headers["accept-encoding"]);
181
+ if (encoding) {
182
+ ctx.res.headers["content-encoding"] = encoding;
183
+ }
184
+
150
185
  res.writeHead(ctx.res.status, ctx.res.headers);
151
- if (ctx.res.body.pipe) {
152
- if (ctx.res.body.path) {
153
- ctx.res.type = ctx.res.body.path.split(".").pop();
154
- ctx.res.size = await fsp.stat(ctx.res.body.path).then((s) => s.size);
155
- } else {
156
- ctx.res.size = 0;
157
- ctx.res.body.on("data", function (chunk) {
158
- ctx.res.size += chunk.length;
159
- });
160
- }
161
- await pipeline(ctx.res.body, res);
186
+
187
+ // If it's not a pipe, e.g. a String, make it a pipe
188
+ if (!ctx.res.body.pipe) {
189
+ ctx.res.body = Readable.from([ctx.res.body]);
190
+ }
191
+
192
+ if (ctx.res.body.path) {
193
+ ctx.res.type = ctx.res.body.path.split(".").pop();
194
+ }
195
+
196
+ if (compress) {
197
+ await pipeline(ctx.res.body, compress(), measure(ctx), res);
162
198
  } else {
163
- res.end(ctx.res.body);
199
+ await pipeline(ctx.res.body, measure(ctx), res);
164
200
  }
201
+ res.end();
202
+
165
203
  // The actual sent headers, as seen by the response
166
204
  ctx.res.headers = Object.fromEntries(
167
205
  res._header
@@ -278,3 +316,12 @@ export const head = (pattern, callback) => {
278
316
  return callback(ctx);
279
317
  };
280
318
  };
319
+
320
+ export const use = (pattern, callback) => {
321
+ return (ctx) => {
322
+ const match = pathPattern(pattern, ctx.url.path);
323
+ if (!match) return null;
324
+ ctx.url.params = match;
325
+ return callback(ctx);
326
+ };
327
+ };