@server/next 0.17.3 → 0.18.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/src/reply.js ADDED
@@ -0,0 +1,95 @@
1
+ import fs from "fs/promises";
2
+
3
+ import { types } from "./helpers/index.js";
4
+
5
+ function Reply() {}
6
+
7
+ Reply.prototype.res = { headers: {}, cookies: {} };
8
+
9
+ // INTERNAL
10
+ Reply.prototype.generateHeaders = function () {
11
+ const cookies = Object.entries(this.res.cookies)
12
+ .map(([k, { value, path = "/" }]) => `${k}=${value};Path=${path}`)
13
+ .join(";");
14
+ return { ...this.res.headers, "set-cookie": cookies };
15
+ };
16
+
17
+ // PARTIAL
18
+ Reply.prototype.status = function (status) {
19
+ this.res.status = status;
20
+ return this;
21
+ };
22
+
23
+ // `.html`, `html`, `text/html`
24
+ Reply.prototype.type = function (type) {
25
+ if (!type) return this;
26
+ this.res.headers["content-type"] = types[type.replace(/^\./)] || type;
27
+ return this;
28
+ };
29
+
30
+ // Set extra headers
31
+ Reply.prototype.headers = function (headers) {
32
+ if (!headers || typeof headers !== "object") return this;
33
+ for (let key in headers) {
34
+ this.res.headers[key] = headers[key];
35
+ }
36
+ return this;
37
+ };
38
+
39
+ // Set extra cookies
40
+ Reply.prototype.cookies = function (cookies) {
41
+ if (!cookies || typeof cookies !== "object") return this;
42
+ for (let key in cookies) {
43
+ if (typeof cookies[key] === "string") {
44
+ this.res.cookies[key] = { value: cookies[key] };
45
+ } else {
46
+ this.res.cookies[key] = cookies[key];
47
+ }
48
+ }
49
+ return this;
50
+ };
51
+
52
+ // FINAL
53
+ Reply.prototype.json = function (body) {
54
+ return headers({ "content-type": "application/json" }).send(
55
+ JSON.stringify(body)
56
+ );
57
+ };
58
+
59
+ Reply.prototype.file = async function (path) {
60
+ const data = await fs.readFile(path, "utf-8");
61
+ if (data) return this.type(path.split(".").pop()).send(data);
62
+ return status(404).send();
63
+ };
64
+
65
+ Reply.prototype.send = function (body = "") {
66
+ const { status = 200 } = this.res;
67
+
68
+ if (typeof body === "string") {
69
+ // Not yet set, so infer the type from type of string
70
+ if (!this.res.headers["content-type"]) {
71
+ const isHtml = body.startsWith("<");
72
+ this.res.headers["content-type"] = isHtml ? "text/html" : "text/plain";
73
+ }
74
+
75
+ const headers = this.generateHeaders();
76
+ return new Response(body, { status, headers });
77
+ }
78
+
79
+ // This is a bit loopy, send({}) => json({}) => send('{}')
80
+ return this.json(body);
81
+ };
82
+
83
+ // INTERNAL
84
+ export { Reply };
85
+
86
+ // PARTIAL
87
+ export const status = (...args) => new Reply().status(...args);
88
+ export const type = (...args) => new Reply().type(...args);
89
+ export const headers = (...args) => new Reply().headers(...args);
90
+ export const cookies = (...args) => new Reply().cookies(...args);
91
+
92
+ // FINAL
93
+ export const send = (...args) => new Reply().send(...args);
94
+ export const json = (...args) => new Reply().json(...args);
95
+ export const file = (...args) => new Reply().file(...args);
package/src/router.js ADDED
@@ -0,0 +1,47 @@
1
+ export default function router() {
2
+ if (!(this instanceof router)) {
3
+ return new router();
4
+ }
5
+ this.handlers = {};
6
+ }
7
+
8
+ // INTERNAL
9
+ router.prototype.handle = function (name, middleware) {
10
+ if (!this.handlers[name]) {
11
+ this.handlers[name] = [];
12
+ }
13
+ this.handlers[name].push(middleware);
14
+ return this;
15
+ };
16
+
17
+ router.prototype.socket = function (path, ...middleware) {
18
+ return this.handle("socket", [path, ...middleware]);
19
+ };
20
+
21
+ router.prototype.get = function (path, ...middleware) {
22
+ return this.handle("get", [path, ...middleware]);
23
+ };
24
+
25
+ router.prototype.head = function (path, ...middleware) {
26
+ return this.handle("head", [path, ...middleware]);
27
+ };
28
+
29
+ router.prototype.post = function (path, ...middleware) {
30
+ return this.handle("post", [path, ...middleware]);
31
+ };
32
+
33
+ router.prototype.put = function (path, ...middleware) {
34
+ return this.handle("put", [path, ...middleware]);
35
+ };
36
+
37
+ router.prototype.patch = function (path, ...middleware) {
38
+ return this.handle("patch", [path, ...middleware]);
39
+ };
40
+
41
+ router.prototype.del = function (path, ...middleware) {
42
+ return this.handle("del", [path, ...middleware]);
43
+ };
44
+
45
+ router.prototype.options = function (path, ...middleware) {
46
+ return this.handle("options", [path, ...middleware]);
47
+ };
@@ -1,123 +0,0 @@
1
- import path from "node:path";
2
- import Spinnies from "spinnies";
3
- import color from "./color.js";
4
-
5
- const isProduction = process.env.NODE_ENV === "production";
6
-
7
- const format = (n, [below, above], limit = Infinity) => {
8
- if (n < 1000) {
9
- return n.toFixed(0).padStart(4, " ") + ` {dim}${below.padEnd(2, " ")}{/}`;
10
- }
11
- const clean = (n / 1000).toFixed(n > 90000 ? 0 : 1).padStart(4, " ");
12
- if (n > 10 * limit) {
13
- return `{red}${clean} ${above.padEnd(2, " ")}{/}`;
14
- } else if (n > limit) {
15
- return `{yellow}${clean} ${above.padEnd(2, " ")}{/}`;
16
- } else {
17
- return `${clean} {dim}${above.padEnd(2, " ")}{/}`;
18
- }
19
- };
20
-
21
- const range = (char, num) => {
22
- const cols = process.stdout.columns || 80;
23
- let str = "";
24
- for (let i = 0; i < cols; i += char.length) {
25
- str += char;
26
- }
27
- return str;
28
- };
29
-
30
- function simpleType(type) {
31
- const simpler = {
32
- "text/html": "html",
33
- "text/plain": "text",
34
- "image/svg+xml": "svg",
35
- "image/png": "png",
36
- "text/css": "css",
37
- "text/javascript": "js",
38
- "application/javascript": "js",
39
- "application/json": "json",
40
- "text/markdown": "md",
41
- };
42
- return simpler[type] || type;
43
- }
44
-
45
- const cwd = path.resolve("./");
46
-
47
- let spinnies;
48
- export default function RequestLogger(ctx) {
49
- this.id = Math.round(Math.random() * 100000);
50
-
51
- if (!isProduction) {
52
- if (!spinnies) {
53
- spinnies = new Spinnies({ succeedColor: "white", failColor: "white" });
54
- }
55
- const method = ("[" + ctx.method.toLowerCase() + "]").padEnd(6, " ");
56
- const text = color(`{dim}${method}{/} ${ctx.url?.path || ctx.url}`);
57
- spinnies.add(`spinner-${this.id}`, { text });
58
- }
59
-
60
- this.end = function (ctx) {
61
- if (isProduction) return;
62
- const status = ctx.res.status;
63
-
64
- const paddedPath = `${ctx.url?.path || ctx.url} {dim}`.padEnd(30, "─");
65
- const statColor = status < 300 ? "green" : status < 500 ? "yellow" : "red";
66
- const statusBlock = `{${statColor}}[${ctx.res.status}]{/}`;
67
- const resSize = format(ctx.res.size || 0, ["b", "kb"], 100000);
68
- const t = Math.round(ctx.time._total - ctx.time._init);
69
- const resTime = format(t, ["ms", "s"], 1000);
70
-
71
- const type = simpleType(
72
- ctx.res.type || ctx.res.headers["content-type"] || "----"
73
- );
74
- const method = ("[" + ctx.method + "]").padEnd(6, " ");
75
- const reqText = `{dim}${method}{/} ${paddedPath}`;
76
- const resText = `${statusBlock} ${resSize} ${resTime} ${type}`;
77
- const text = color(`${reqText}─›{/} ${resText}`);
78
-
79
- this.text = text;
80
- if (statColor !== "red") {
81
- spinnies.succeed(`spinner-${this.id}`, { text });
82
- } else {
83
- spinnies.fail(`spinner-${this.id}`, { text });
84
- }
85
- };
86
-
87
- this.error = function (err) {
88
- const cols = process.stdout.columns || 80;
89
- console.log(color("{red} ┌───────┬" + range("─").slice(12) + "┐{/}"));
90
- console.log(
91
- color(
92
- `{red} │ {bright}{red}Error{/} {red}│{/} ${err.message.padEnd(
93
- cols - 14,
94
- " "
95
- )} {red}│{/}`
96
- )
97
- );
98
- console.log(color("{red} ├───────┼" + range("─").slice(12) + "┤{/}"));
99
- console.log(
100
- color(
101
- err.stack
102
- .split("\n")
103
- .slice(1)
104
- .map((line, i) => {
105
- line = line.replace(/\s*at\s/, "");
106
- line = line.replace("file://" + cwd, "{dim}$PWD{/}");
107
- return (
108
- `{red} │{/} ${i === 0 ? "Trace" : " "} {red}│{/} ` +
109
- line.padEnd(cols - 6, " ") +
110
- " {red}│{/}"
111
- );
112
- })
113
- .join("\n")
114
- )
115
- );
116
- console.log(color("{red} └───────┴" + range("─").slice(12) + "┘{/}"));
117
- };
118
-
119
- this.reprint = function () {
120
- if (isProduction) return;
121
- console.log(color(`{red}✖{/} `) + this.text);
122
- };
123
- }
package/src/ServerUrl.js DELETED
@@ -1,68 +0,0 @@
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 {
27
- constructor(urlString) {
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}`;
55
- }
56
-
57
- getQuery(entries) {
58
- const query = {};
59
- for (const [key, value] of entries) {
60
- query[key] = value;
61
- }
62
- return query;
63
- }
64
-
65
- toString() {
66
- return this.href;
67
- }
68
- }
@@ -1,63 +0,0 @@
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
- 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
- ]);
38
- });
39
-
40
- it("can parse localhost", () => {
41
- const url = new ServerUrl("http://localhost:3000/");
42
-
43
- // Extended values
44
- expect(url.path).toEqual("/");
45
- expect(url.query).toEqual({});
46
- expect(url.params).toEqual({});
47
-
48
- // Base values
49
- expect(url.href).toBe("http://localhost:3000/");
50
- expect(url.pathname).toEqual("/");
51
- expect(url.protocol).toBe("http:");
52
- expect(url.username).toBe("");
53
- expect(url.password).toBe("");
54
- expect(url.host).toBe("localhost:3000");
55
- expect(url.hostname).toBe("localhost");
56
- expect(url.port).toBe(3000);
57
- });
58
-
59
- it("can be stringified", () => {
60
- const url = new ServerUrl("http://localhost:3000/");
61
- expect(url + "").toBe("http://localhost:3000/");
62
- });
63
- });
package/src/getMime.js DELETED
@@ -1,48 +0,0 @@
1
- import fs from "node:fs";
2
- import fsp from "node:fs/promises";
3
- import { spawn } from "node:child_process";
4
-
5
- // These are some mime types that are not properly produced by
6
- // the file --mime-type command, and can be inferred by their
7
- // filename extension easily
8
- // Note: text/javascript is the recommended now https://stackoverflow.com/a/876805/938236
9
- const extensions = {
10
- css: "text/css",
11
- js: "text/javascript",
12
- };
13
-
14
- const exists = (file) => {
15
- return fsp.stat(file, fs.constants.F_OK).then(
16
- (stat) => stat.isFile(),
17
- () => false
18
- );
19
- };
20
-
21
- function cmd(...args) {
22
- const { stdout: stream } = spawn(...args);
23
- const chunks = [];
24
- return new Promise((resolve, reject) => {
25
- stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
26
- stream.on("error", (err) => reject(err));
27
- stream.on("end", () =>
28
- resolve(Buffer.concat(chunks).toString("utf8").trim())
29
- );
30
- });
31
- }
32
-
33
- export default async function (file) {
34
- const cwd = await fsp.realpath("./");
35
- file = await fsp.realpath(file);
36
-
37
- const ext = file.split(".").pop();
38
- if (extensions[ext]) return extensions[ext];
39
-
40
- // It _is_ a file
41
- if (!(await exists(file))) return false;
42
- // Inside the CWD directory
43
- if (!file.startsWith(cwd)) return false;
44
-
45
- // The two conditions above give us enough confidence to run
46
- // this, but even then we use spawn() to avoid some nasty injections
47
- return cmd("file", ["--mime-type", "-b", file]);
48
- }
package/src/logger.js DELETED
@@ -1,21 +0,0 @@
1
- import color from "./color.js";
2
-
3
- const isProduction = process.env.NODE_ENV === "production";
4
-
5
- const header = function ({ api, options }) {
6
- if (isProduction) return;
7
- const routes = Object.values(api).flat().length;
8
- console.clear();
9
- console.log(
10
- color(
11
- `{green}Started server successfully{/} (${routes} routes). See app in the browser:
12
-
13
- 🔗 {under}http://localhost:${options.port}/{/}
14
-
15
- {dim}Press [ctrl+c] to exit the server.{/}
16
- `
17
- )
18
- );
19
- };
20
-
21
- export default { header };
File without changes