@server/next 0.21.14 → 0.22.1

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.21.14",
3
+ "version": "0.22.1",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "start": "bun test --watch",
22
+ "lint": "npx @biomejs/biome lint ./src --skip=lint/style/noParameterAssign",
22
23
  "test": "bun test",
23
24
  "test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
24
25
  },
@@ -3,10 +3,11 @@ export default class ServerError extends Error {
3
3
  if (typeof message === "function") {
4
4
  message = message(vars);
5
5
  }
6
- for (let key in vars) {
7
- if (typeof message === "string" && typeof vars[key] === "string") {
8
- message = message.replaceAll(`{${key}}`, vars[key]);
9
- }
6
+ if (typeof message !== "string") throw Error(`Invalid error ${message}`);
7
+ for (const key in vars) {
8
+ let val = vars[key];
9
+ if (Array.isArray(val)) val = vars[key].join(",");
10
+ message = message.replaceAll(`{${key}}`, val);
10
11
  }
11
12
 
12
13
  super(message);
@@ -15,7 +16,7 @@ export default class ServerError extends Error {
15
16
  this.status = status;
16
17
  }
17
18
  static extend(errors) {
18
- for (let code in errors) {
19
+ for (const code in errors) {
19
20
  const message = errors[code]?.message || errors[code];
20
21
  const status = errors[code]?.status;
21
22
  ServerError[code] = (vars) =>
@@ -21,7 +21,7 @@ describe("user creation flow", () => {
21
21
  expect(register).toSucceed();
22
22
  expect(await store.keys()).toEqual([
23
23
  "user:abc@test.com",
24
- "auth:" + register.headers["set-cookie"].split(";")[0].split("=")[1],
24
+ `auth:${register.headers["set-cookie"].split(";")[0].split("=")[1]}`,
25
25
  ]);
26
26
 
27
27
  const me = await api.get("/me");
@@ -36,7 +36,7 @@ describe("user creation flow", () => {
36
36
  expect(login).toSucceed();
37
37
  expect(await store.keys()).toEqual([
38
38
  "user:abc@test.com",
39
- "auth:" + login.headers["set-cookie"].split(";")[0].split("=")[1],
39
+ `auth:${login.headers["set-cookie"].split(";")[0].split("=")[1]}`,
40
40
  ]);
41
41
  });
42
42
  });
@@ -34,7 +34,7 @@ describe("user creation flow", () => {
34
34
 
35
35
  // CAN GET MY OWN INFO
36
36
  await (async () => {
37
- const headers = { authorization: "Bearer " + token };
37
+ const headers = { authorization: `Bearer ${token}` };
38
38
  const me = await api.get("/me", { headers });
39
39
  expect(me).toSucceed();
40
40
  expect(me.body.email).toEqual(EMAIL);
@@ -45,7 +45,7 @@ describe("user creation flow", () => {
45
45
 
46
46
  // LOGOUT TEST
47
47
  await (async () => {
48
- const headers = { authorization: "Bearer " + token };
48
+ const headers = { authorization: `Bearer ${token}` };
49
49
  const logout = await api.post("/auth/logout", {}, { headers });
50
50
  expect(logout).toSucceed();
51
51
  expect(await users()).toEqual(["abc@test.com"]);
@@ -63,7 +63,7 @@ describe("user creation flow", () => {
63
63
 
64
64
  // CAN GET MY OWN INFO
65
65
  await (async () => {
66
- const headers = { authorization: "Bearer " + token };
66
+ const headers = { authorization: `Bearer ${token}` };
67
67
  const me = await api.get("/me", { headers });
68
68
  expect(me).toSucceed();
69
69
  expect(me.body.email).toEqual(EMAIL);
@@ -71,7 +71,7 @@ describe("user creation flow", () => {
71
71
 
72
72
  // UPDATE PASSWORD
73
73
  await (async () => {
74
- const headers = { authorization: "Bearer " + token };
74
+ const headers = { authorization: `Bearer ${token}` };
75
75
  const body = { previous: PASS, updated: "22222222" };
76
76
  const update = await api.put("/auth/password/email", body, { headers });
77
77
  expect(update).toSucceed();
@@ -81,7 +81,7 @@ describe("user creation flow", () => {
81
81
 
82
82
  // LOGOUT AGAIN
83
83
  await (async () => {
84
- const headers = { authorization: "Bearer " + token };
84
+ const headers = { authorization: `Bearer ${token}` };
85
85
  const logout = await api.post("/auth/logout", {}, { headers });
86
86
  expect(logout).toSucceed();
87
87
  expect(await users()).toEqual(["abc@test.com"]);
package/src/auth/auth.js CHANGED
@@ -53,8 +53,10 @@ export default async function auth(ctx) {
53
53
 
54
54
  if (!auth.provider) throw ServerError.AUTH_NO_PROVIDER();
55
55
  if (!options.provider.includes(auth.provider)) {
56
- const valid = JSON.stringify(options.provider);
57
- throw ServerError.AUTH_INVALID_PROVIDER({ provider: auth.provider, valid });
56
+ throw ServerError.AUTH_INVALID_PROVIDER({
57
+ provider: auth.provider,
58
+ valid: options.provider,
59
+ });
58
60
  }
59
61
  return auth;
60
62
  }
@@ -31,7 +31,7 @@ describe("auth", () => {
31
31
  const authorization = "Bearer REqA2l022l8Q0tuI";
32
32
  const req = await api.get("/", { headers: { authorization } });
33
33
  expect(req).not.toSucceed(
34
- 'Invalid provider "wrong", valid ones are: "email"',
34
+ "Invalid provider 'wrong', valid ones are: 'email'",
35
35
  );
36
36
  });
37
37
  });
@@ -6,13 +6,15 @@ export default async function logout(ctx) {
6
6
 
7
7
  if (type === "token") {
8
8
  return { token: null };
9
- } else if (type === "cookie") {
9
+ }
10
+ if (type === "cookie") {
10
11
  return cookies({ authorization: null }).redirect("/");
11
- } else if (type === "jwt") {
12
+ }
13
+ if (type === "jwt") {
12
14
  throw new Error("JWT auth not supported yet");
13
- } else if (type === "key") {
15
+ }
16
+ if (type === "key") {
14
17
  throw new Error("Key auth not supported yet");
15
- } else {
16
- throw new Error("Unknown auth type");
17
18
  }
19
+ throw new Error("Unknown auth type");
18
20
  }
@@ -48,15 +48,17 @@ const createSession = async (user, ctx) => {
48
48
 
49
49
  if (type === "token") {
50
50
  return status(201).json({ ...user, token: id });
51
- } else if (type === "cookie") {
51
+ }
52
+ if (type === "cookie") {
52
53
  return status(302).cookies({ authentication: id }).redirect(redirect);
53
- } else if (type === "jwt") {
54
+ }
55
+ if (type === "jwt") {
54
56
  throw new Error("JWT auth not supported yet");
55
- } else if (type === "key") {
57
+ }
58
+ if (type === "key") {
56
59
  throw new Error("Key auth not supported yet");
57
- } else {
58
- throw new Error("Unknown auth type");
59
60
  }
61
+ throw new Error("Unknown auth type");
60
62
  };
61
63
 
62
64
  async function login(ctx) {
@@ -19,8 +19,8 @@ const oauth = async (code) => {
19
19
  }),
20
20
  });
21
21
  return (path) => {
22
- return fch("https://api.github.com" + path, {
23
- headers: { Authorization: "Bearer " + res.access_token },
22
+ return fch(`https://api.github.com${path}`, {
23
+ headers: { Authorization: `Bearer ${res.access_token}` },
24
24
  });
25
25
  };
26
26
  };
@@ -68,15 +68,17 @@ const callback = async (ctx) => {
68
68
 
69
69
  if (auth.type === "token") {
70
70
  return status(201).json({ ...user, token: auth.id });
71
- } else if (auth.type === "cookie") {
71
+ }
72
+ if (auth.type === "cookie") {
72
73
  return status(302).cookies({ authentication: auth.id }).redirect(redirect);
73
- } else if (auth.type === "jwt") {
74
+ }
75
+ if (auth.type === "jwt") {
74
76
  throw new Error("JWT auth not supported yet");
75
- } else if (auth.type === "key") {
77
+ }
78
+ if (auth.type === "key") {
76
79
  throw new Error("Key auth not supported yet");
77
- } else {
78
- throw new Error("Unknown auth type");
79
80
  }
81
+ throw new Error("Unknown auth type");
80
82
  };
81
83
 
82
84
  export default { login, callback };
@@ -10,9 +10,9 @@ const chunkArray = (arr, size) =>
10
10
  ? [arr.slice(0, size), ...chunkArray(arr.slice(size), size)]
11
11
  : [arr];
12
12
 
13
- export default async (request, options = {}, app) => {
13
+ export default async (request, app) => {
14
14
  const ctx = {};
15
- ctx.options = options;
15
+ ctx.options = app.opts || {};
16
16
  ctx.req = request;
17
17
  ctx.res = { status: null, headers: {}, cookies: {} };
18
18
  ctx.method = request.method.toLowerCase();
@@ -25,7 +25,9 @@ export default async (request, options = {}, app) => {
25
25
  };
26
26
  ctx.unstableFire = (name, data) => {
27
27
  if (!events[name]) return;
28
- events[name].forEach((cb) => cb(data));
28
+ for (const cb of events[name]) {
29
+ cb(data);
30
+ }
29
31
  };
30
32
 
31
33
  ctx.headers = parseHeaders(new Headers(chunkArray(request.rawHeaders, 2)));
@@ -33,7 +35,7 @@ export default async (request, options = {}, app) => {
33
35
  await auth.load(ctx);
34
36
 
35
37
  const https = request.connection.encrypted ? "https" : "http";
36
- const host = ctx.headers.host || "localhost" + options.port;
38
+ const host = ctx.headers.host || `localhost:${ctx.options.port}`;
37
39
  const path = request.url.replace(/\/$/, "") || "/";
38
40
  ctx.url = new URL(path, `${https}://${host}`);
39
41
  define(ctx.url, "query", (url) =>
@@ -51,7 +53,7 @@ export default async (request, options = {}, app) => {
51
53
  ctx.body = await parseBody(
52
54
  Buffer.concat(body).toString(),
53
55
  type,
54
- options.uploads,
56
+ ctx.options.uploads,
55
57
  );
56
58
  resolve();
57
59
  })
@@ -2,12 +2,12 @@ import { createId } from "../helpers/index.js";
2
2
 
3
3
  function getBoundary(header) {
4
4
  if (!header) return null;
5
- var items = header.split(";");
5
+ const items = header.split(";");
6
6
  if (items)
7
- for (var j = 0; j < items.length; j++) {
8
- var item = new String(items[j]).trim();
7
+ for (let j = 0; j < items.length; j++) {
8
+ const item = new String(items[j]).trim();
9
9
  if (item.indexOf("boundary") >= 0) {
10
- var k = item.split("=");
10
+ const k = item.split("=");
11
11
  return new String(k[1]).trim();
12
12
  }
13
13
  }
@@ -48,7 +48,7 @@ export default async function parseBody(raw, contentType, bucket) {
48
48
  const body = {};
49
49
 
50
50
  const rawDataArray = rawData.split(boundary);
51
- for (let item of rawDataArray) {
51
+ for (const item of rawDataArray) {
52
52
  // Use non-matching groups to exclude part of the result
53
53
  const name = getMatching(item, /(?:name=")(.+?)(?:")/)
54
54
  .trim()
@@ -3,10 +3,10 @@ import { define, parseHeaders } from "../helpers/index.js";
3
3
  import parseBody from "./parseBody.js";
4
4
  import parseCookies from "./parseCookies.js";
5
5
 
6
- export default async (request, options = {}, app) => {
6
+ export default async (request, app) => {
7
7
  const ctx = {};
8
8
  ctx.init = performance.now();
9
- ctx.options = options;
9
+ ctx.options = app.opts || {};
10
10
  ctx.req = request;
11
11
  ctx.res = { status: null, headers: {}, cookies: {} };
12
12
  ctx.method = request.method.toLowerCase();
@@ -19,7 +19,9 @@ export default async (request, options = {}, app) => {
19
19
  };
20
20
  ctx.unstableFire = (name, data) => {
21
21
  if (!events[name]) return;
22
- events[name].forEach((cb) => cb(data));
22
+ for (const cb of events[name]) {
23
+ cb(data);
24
+ }
23
25
  };
24
26
 
25
27
  ctx.headers = parseHeaders(request.headers);
@@ -33,7 +35,7 @@ export default async (request, options = {}, app) => {
33
35
 
34
36
  if (request.body) {
35
37
  const type = ctx.headers["content-type"];
36
- ctx.body = await parseBody(request, type, options.uploads);
38
+ ctx.body = await parseBody(request, type, ctx.options.uploads);
37
39
  }
38
40
 
39
41
  ctx.app = app;
@@ -1,17 +1,18 @@
1
1
  import ServerError from "../ServerError.js";
2
2
 
3
3
  ServerError.extend({
4
- NO_STORE: `You need a 'store' to write 'ctx.session'`,
5
- NO_STORE_WRITE: `You need a 'store' to write 'ctx.session.{key}'`,
6
- NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
4
+ NO_STORE: "You need a 'store' to write 'ctx.session'",
5
+ NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
6
+ NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
7
7
 
8
8
  AUTH_ARGON_NEEDED:
9
9
  "Argon2 is needed for the auth module, please install it with 'npm i argon2'",
10
- AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
11
- AUTH_INVALID_TOKEN: `Invalid Authorization token`,
12
- AUTH_INVALID_COOKIE: `Invalid Authorization cookie`,
13
- AUTH_NO_PROVIDER: `No provider passed to the option "auth.provider"`,
14
- AUTH_INVALID_PROVIDER: `Invalid provider "{provider}", valid ones are: {valid}`,
10
+ AUTH_INVALID_TYPE: "Invalid Authorization type, '{type}'",
11
+ AUTH_INVALID_TOKEN: "Invalid Authorization token",
12
+ AUTH_INVALID_COOKIE: "Invalid Authorization cookie",
13
+ AUTH_NO_PROVIDER: "No provider passed to the option 'auth.provider'",
14
+ AUTH_INVALID_PROVIDER:
15
+ "Invalid provider '{provider}', valid ones are: '{valid}'",
15
16
  AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
16
17
  AUTH_NO_USER: {
17
18
  status: 401,
@@ -22,14 +23,14 @@ ServerError.extend({
22
23
  LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
23
24
  LOGIN_NO_PASSWORD: "The email is required to log in",
24
25
  LOGIN_INVALID_PASSWORD: "The password you wrote is not correct",
25
- LOGIN_WRONG_ACCOUNT: `That email does not correspond to any account`,
26
- LOGIN_WRONG_PASSWORD: `That is not the valid password`,
26
+ LOGIN_WRONG_ACCOUNT: "That email does not correspond to any account",
27
+ LOGIN_WRONG_PASSWORD: "That is not the valid password",
27
28
 
28
- REGISTER_NO_EMAIL: `Email needed`,
29
+ REGISTER_NO_EMAIL: "Email needed",
29
30
  REGISTER_INVALID_EMAIL: "The email you wrote is not correct",
30
- REGISTER_NO_PASSWORD: `Password needed`,
31
+ REGISTER_NO_PASSWORD: "Password needed",
31
32
  REGISTER_INVALID_PASSWORD: "The password you wrote is not correct",
32
- REGISTER_EMAIL_EXISTS: `Email is already registered`,
33
+ REGISTER_EMAIL_EXISTS: "Email is already registered",
33
34
  });
34
35
 
35
36
  export default ServerError;
@@ -1,7 +1,6 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- import fsp from "fs/promises";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import fsp from "node:fs/promises";
5
4
 
6
5
  // A fake tiny implementation of a generic bucket, it needs
7
6
  // at the very least a read(id) and write(id, value), both returning
@@ -13,7 +12,7 @@ export default function (root) {
13
12
  }
14
13
 
15
14
  const absolute = (name) => {
16
- if (!name) throw new Error(`File name is required`);
15
+ if (!name) throw new Error("File name is required");
17
16
  return path.resolve(path.join(root, name));
18
17
  };
19
18
 
@@ -27,9 +26,8 @@ export default function (root) {
27
26
  const fullPath = absolute(name);
28
27
  if (value) {
29
28
  return fsp.writeFile(fullPath, value, type).then(() => fullPath);
30
- } else {
31
- return fs.createWriteStream(fullPath);
32
29
  }
30
+ return fs.createWriteStream(fullPath);
33
31
  },
34
32
  };
35
33
  }
@@ -8,7 +8,7 @@ export default function config(options = {}) {
8
8
 
9
9
  // Basic options
10
10
  options.port = options.port || env.PORT || 3000;
11
- options.secret = options.secret || env.SECRET || "unsafe-" + createId();
11
+ options.secret = options.secret || env.SECRET || `unsafe-${createId()}`;
12
12
 
13
13
  // CORS
14
14
  options.cors = options.cors || env.CORS || null;
@@ -9,8 +9,8 @@ export default function createCookies(cookies) {
9
9
  val = { value: val };
10
10
  }
11
11
  const { value, path, expires } = val;
12
- const pathPart = ";Path=" + (path || "/");
13
- const expiresPart = expires ? ";Expires=" + expires : "";
12
+ const pathPart = `;Path=${path || "/"}`;
13
+ const expiresPart = expires ? `;Expires=${expires}` : "";
14
14
  return `${key}=${value || ""}${pathPart}${expiresPart}`;
15
15
  });
16
16
  }
@@ -1,7 +1,7 @@
1
1
  const alphabet =
2
2
  "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
3
3
 
4
- export let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
4
+ export const random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
5
5
 
6
6
  // Credit: https://stackoverflow.com/a/52171480/938236
7
7
  const cyrb53 = (str, seed = 0) => {
@@ -35,7 +35,7 @@ const hash = (str, size) => {
35
35
 
36
36
  const randomId = (size = 16) => {
37
37
  let id = "";
38
- let bytes = random(size);
38
+ const bytes = random(size);
39
39
  while (size--) {
40
40
  // Using the bitwise AND operator to "cap" the value of
41
41
  // the random byte from 255 to 63, in that way we can make sure
@@ -4,14 +4,14 @@ import define from "./define.js";
4
4
  import validate from "./validate.js";
5
5
 
6
6
  export default async function handleRequest(handlers, ctx) {
7
- for (let [method, matcher, ...cbs] of handlers[ctx.method]) {
7
+ for (const [method, matcher, ...cbs] of handlers[ctx.method]) {
8
8
  const match = pathPattern(matcher, ctx.url.pathname || "/");
9
9
  // Skip this whole middleware if there was no match
10
10
  if (!match) continue;
11
11
 
12
12
  define(ctx.url, "params", () => match);
13
13
 
14
- for (let cb of cbs) {
14
+ for (const cb of cbs) {
15
15
  if (typeof cb === "function") {
16
16
  const res = await cb(ctx);
17
17
  const out = await parseResponse(res, ctx);
@@ -65,7 +65,7 @@ export const jsx = (tag, { children, ...props }) => {
65
65
  : `${altAttrs[k.toLowerCase()] || encode(k)}="${encode(v)}"`,
66
66
  )
67
67
  .join(" ");
68
- if (attrStr) attrStr = " " + attrStr;
68
+ if (attrStr) attrStr = ` ${attrStr}`;
69
69
  if (SELFCLOSE.has(tag)) return () => `<${tag}${attrStr} />`;
70
70
  const doctype = tag === "html" ? "<!DOCTYPE html>" : "";
71
71
  return () => `${doctype}<${tag}${attrStr}>${children}</${tag}>`;
@@ -16,7 +16,7 @@ expect.extend({
16
16
 
17
17
  describe("jsx", () => {
18
18
  it("can render a div", () => {
19
- expect(<div>Hello</div>).toRender(`<div>Hello</div>`);
19
+ expect(<div>Hello</div>).toRender("<div>Hello</div>");
20
20
  });
21
21
 
22
22
  it("can render an input with attributes", () => {
@@ -40,6 +40,7 @@ describe("jsx", () => {
40
40
  });
41
41
 
42
42
  it("will inject the doctype for html", () => {
43
+ // biome-ignore lint/a11y/useHtmlLang: This is an example, not user code
43
44
  expect(<html>Hello</html>).toRender("<!DOCTYPE html><html>Hello</html>");
44
45
  expect(<html lang="en">Hello</html>).toRender(
45
46
  `<!DOCTYPE html><html lang="en">Hello</html>`,
@@ -30,6 +30,7 @@ export default {
30
30
  js: "text/javascript",
31
31
  json: "application/json",
32
32
  jsonld: "application/ld+json",
33
+ md: "text/markdown",
33
34
  mid: "audio/midi",
34
35
  midi: "audio/midi",
35
36
  mjs: "text/javascript",
@@ -55,6 +56,7 @@ export default {
55
56
  sh: "application/x-sh",
56
57
  svg: "image/svg+xml",
57
58
  tar: "application/x-tar",
59
+ text: "text/plain",
58
60
  tif: "image/tiff",
59
61
  tiff: "image/tiff",
60
62
  ts: "video/mp2t",
package/src/index.js CHANGED
@@ -84,7 +84,7 @@ export default function server(options = {}) {
84
84
  server.prototype.self = function () {
85
85
  const cb = this.callback.bind(this);
86
86
  const proto = Object.getPrototypeOf(this);
87
- for (let key in { ...proto, ...this }) {
87
+ for (const key in { ...proto, ...this }) {
88
88
  if (typeof this[key] === "function") {
89
89
  cb[key] = this[key].bind(this);
90
90
  } else {
@@ -97,11 +97,11 @@ server.prototype.self = function () {
97
97
  // #region Runtimes
98
98
  // Node.js
99
99
  server.prototype.node = async function () {
100
- const http = await import("http");
100
+ const http = await import("node:http");
101
101
  http
102
102
  .createServer(async (request, response) => {
103
103
  try {
104
- const ctx = await createNodeContext(request, this.opts, this);
104
+ const ctx = await createNodeContext(request, this);
105
105
  const out = await handleRequest(this.handlers, ctx);
106
106
 
107
107
  response.writeHead(out.status || 200, parseHeaders(out.headers));
@@ -128,7 +128,7 @@ server.prototype.callback = async function (request, context) {
128
128
  if (typeof Netlify === "undefined") {
129
129
  throw new Error("Netlify doesn't exist");
130
130
  }
131
- const ctx = await createWinterContext(request, this.opts, this);
131
+ const ctx = await createWinterContext(request, this);
132
132
  return await handleRequest(this.handlers, ctx);
133
133
  } catch (error) {
134
134
  return new Response(error.message, { status: error.status || 500 });
@@ -140,9 +140,11 @@ server.prototype.fetch = async function (request, env) {
140
140
  if (env?.upgrade(request)) return;
141
141
  Object.assign(globalThis.env, env); // Extend env with the passed vars
142
142
 
143
- let ctx, res, error;
143
+ let ctx;
144
+ let res;
145
+ let error;
144
146
  try {
145
- ctx = await createWinterContext(request, this.opts, this);
147
+ ctx = await createWinterContext(request, this);
146
148
  res = await handleRequest(this.handlers, ctx);
147
149
  } catch (err) {
148
150
  error = err;
@@ -158,7 +160,7 @@ server.prototype.handle = function (method, path, ...middleware) {
158
160
  // Do not try to optimize, we NEED the method to remain '*' here so that
159
161
  // it doesn't auto-finish
160
162
  if (method === "*") {
161
- for (let m in this.handlers) {
163
+ for (const m in this.handlers) {
162
164
  this.handlers[m].push([method, path, ...middleware]);
163
165
  }
164
166
  } else {
@@ -209,11 +211,11 @@ server.prototype.use = function (...middleware) {
209
211
 
210
212
  // Unwind the children routers into the main router
211
213
  server.prototype.router = function (basePath, router) {
212
- basePath = ("/" + basePath + "/").replace(/^\/+/, "/").replace(/\/+$/, "/");
213
- for (const method in router.handlers) {
214
- router.handlers[method].forEach(([method, path, ...callbacks]) => {
214
+ basePath = `/${basePath}/`.replace(/^\/+/, "/").replace(/\/+$/, "/");
215
+ for (const m in router.handlers) {
216
+ for (const [method, path, ...callbacks] of router.handlers[m]) {
215
217
  this.handle(method, basePath + path.replace(/^\//, ""), ...callbacks);
216
- });
218
+ }
217
219
  }
218
220
  return this.self();
219
221
  };
@@ -231,7 +233,7 @@ server.prototype.test = function () {
231
233
  options.headers.cookie = cookie;
232
234
  }
233
235
  const res = await this.fetch(
234
- new Request("http://localhost:3000" + path, options),
236
+ new Request(`http://localhost:3000${path}`, options),
235
237
  );
236
238
 
237
239
  const headers = parseHeaders(res.headers);
@@ -81,15 +81,15 @@ export default async function parseResponse(out, ctx) {
81
81
  // Cookies to headers
82
82
  if (ctx.options.cookies) {
83
83
  if (Object.keys(ctx.res.cookies).length) {
84
- createCookies(ctx.res.cookies).forEach((cookie) => {
84
+ for (const cookie of ctx.res.cookies) {
85
85
  ctx.res.headers.append("set-cookie", cookie);
86
- });
86
+ }
87
87
  }
88
88
  }
89
89
 
90
90
  // Add the headers that are neeeded
91
91
  if (ctx?.res?.headers) {
92
- for (let key in ctx.res.headers) {
92
+ for (const key in ctx.res.headers) {
93
93
  out.headers[key] = ctx.res.headers[key];
94
94
  }
95
95
  }
@@ -1,7 +1,7 @@
1
1
  export default function pathPattern(pattern, path) {
2
2
  if (pattern === "*") return {};
3
3
 
4
- pattern = "/" + pattern.replace(/^\//, "");
4
+ pattern = `/${pattern.replace(/^\//, "")}`;
5
5
  pattern = pattern.replace(/\/$/, "") || "/";
6
6
  path = path.replace(/\/$/, "") || "/";
7
7
 
package/src/reply.js CHANGED
@@ -1,4 +1,4 @@
1
- import fs from "fs/promises";
1
+ import fs from "node:fs/promises";
2
2
 
3
3
  import { createCookies, toWeb, types } from "./helpers/index.js";
4
4
 
@@ -12,9 +12,9 @@ function Reply() {
12
12
  // INTERNAL
13
13
  Reply.prototype.generateHeaders = function () {
14
14
  const headers = new Headers(this.res.headers);
15
- createCookies(this.res.cookies).forEach((cookie) => {
15
+ for (const cookie of createCookies(this.res.cookies)) {
16
16
  headers.append("set-cookie", cookie);
17
- });
17
+ }
18
18
  return headers;
19
19
  };
20
20
 
@@ -27,14 +27,26 @@ Reply.prototype.status = function (status) {
27
27
  // `.html`, `html`, `text/html`
28
28
  Reply.prototype.type = function (type) {
29
29
  if (!type) return this;
30
- this.res.headers["content-type"] = types[type.replace(/^\./)] || type;
31
- return this;
30
+ type = types[type.replace(/^\./)] || type;
31
+ return this.headers({ "content-type": type });
32
+ };
33
+
34
+ // Prompt for download from the user side
35
+ Reply.prototype.download = function (name, type) {
36
+ // filename.txt and no explicit type => add headers "text/plain"
37
+ if (name && !type) type = name.split(".")[1];
38
+
39
+ // Add the Content-Type if there's a type
40
+ if (type) this.type(type);
41
+
42
+ const filename = name ? `; filename="${name}"` : "";
43
+ return this.headers({ "content-disposition": `attachment${filename}` });
32
44
  };
33
45
 
34
46
  // Set extra headers
35
47
  Reply.prototype.headers = function (headers) {
36
48
  if (!headers || typeof headers !== "object") return this;
37
- for (let key in headers) {
49
+ for (const key in headers) {
38
50
  this.res.headers[key] = headers[key];
39
51
  }
40
52
  return this;
@@ -43,7 +55,7 @@ Reply.prototype.headers = function (headers) {
43
55
  // Set extra cookies
44
56
  Reply.prototype.cookies = function (cookies) {
45
57
  if (!cookies || typeof cookies !== "object") return this;
46
- for (let key in cookies) {
58
+ for (const key in cookies) {
47
59
  if (typeof cookies[key] === "string") {
48
60
  this.res.cookies[key] = { value: cookies[key] };
49
61
  } else {
@@ -127,8 +139,9 @@ export { Reply };
127
139
 
128
140
  // PARTIAL
129
141
  export const status = (...args) => new Reply().status(...args);
130
- export const type = (...args) => new Reply().type(...args);
131
142
  export const headers = (...args) => new Reply().headers(...args);
143
+ export const type = (...args) => new Reply().type(...args);
144
+ export const download = (...args) => new Reply().download(...args);
132
145
  export const cookies = (...args) => new Reply().cookies(...args);
133
146
 
134
147
  // FINAL
@@ -2,9 +2,9 @@ import server, { router, status } from "./index.js";
2
2
 
3
3
  describe("can route properly", () => {
4
4
  const apiRouter = router()
5
- .get("/hello", (ctx) => "Hello " + ctx.url.pathname)
6
- .put("/hello", (ctx) => "Hello " + ctx.url.pathname)
7
- .post("/hello", (ctx) => "Hello " + ctx.url.pathname);
5
+ .get("/hello", (ctx) => `Hello ${ctx.url.pathname}`)
6
+ .put("/hello", (ctx) => `Hello ${ctx.url.pathname}`)
7
+ .post("/hello", (ctx) => `Hello ${ctx.url.pathname}`);
8
8
 
9
9
  const app = server()
10
10
  .router("/", apiRouter)
@@ -5,11 +5,11 @@ import server from "./index.js";
5
5
  describe("session", () => {
6
6
  const store = kv(new Map());
7
7
  const api = server({ store })
8
- .get("/hello", (ctx) => "Hello " + ctx.session.a)
8
+ .get("/hello", (ctx) => `Hello ${ctx.session.a}`)
9
9
  .post("/hello", (ctx) => {
10
10
  if (!ctx.session.a) ctx.session.a = 0;
11
11
  ctx.session.a += 1;
12
- return "Bye " + ctx.session.a;
12
+ return `Bye ${ctx.session.a}`;
13
13
  })
14
14
  .get("/", () => "Fallback")
15
15
  .test();
@@ -44,7 +44,7 @@ describe("session", () => {
44
44
 
45
45
  describe("missing store", () => {
46
46
  const api = server({ store: null })
47
- .get("/read", (ctx) => "Bye " + ctx.session.a)
47
+ .get("/read", (ctx) => `Bye ${ctx.session.a}`)
48
48
  .get("/write", (ctx) => {
49
49
  ctx.session.a = "hello";
50
50
  return "All good";