@server/next 0.17.2 → 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/package.json +7 -10
- package/readme.md +122 -20
- package/src/bucket.js +5 -3
- package/src/context/node.js +28 -0
- package/src/{parseBody.js → context/parseBody.js} +8 -16
- package/src/{parseBody.test.js → context/parseBody.test.js} +3 -3
- package/src/context/parseCookies.js +9 -0
- package/src/context/winter.js +25 -0
- package/src/helpers/define.js +18 -0
- package/src/helpers/getMachine.js +13 -0
- package/src/helpers/handleRequest.js +20 -0
- package/src/helpers/index.js +5 -0
- package/src/helpers/iterate.js +8 -0
- package/src/helpers/types.js +79 -0
- package/src/index.js +104 -329
- package/src/index.test.js +45 -0
- package/src/parseResponse.js +48 -0
- package/src/pathPattern.js +27 -20
- package/src/polyfill.js +6 -0
- package/src/reply.js +95 -0
- package/src/router.js +47 -0
- package/src/RequestLogger.js +0 -123
- package/src/ServerUrl.js +0 -68
- package/src/ServerUrl.test.js +0 -63
- package/src/getMime.js +0 -48
- package/src/logger.js +0 -21
- /package/src/{color.js → helpers/color.js} +0 -0
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
|
+
};
|
package/src/RequestLogger.js
DELETED
|
@@ -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
|
-
}
|
package/src/ServerUrl.test.js
DELETED
|
@@ -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
|