@server/next 0.21.2 → 0.21.4

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.2",
3
+ "version": "0.21.4",
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",
@@ -16,17 +16,17 @@ describe("user creation flow", () => {
16
16
  .test();
17
17
 
18
18
  // Bun's bug: https://github.com/oven-sh/bun/issues/6348
19
- it.skip("can create a new user", async () => {
19
+ it("can create a new user", async () => {
20
20
  const register = await api.post("/auth/register/email", CREDENTIALS);
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("=")[1],
24
+ "auth:" + register.headers["set-cookie"].split(";")[0].split("=")[1],
25
25
  ]);
26
26
 
27
27
  const me = await api.get("/me");
28
28
  expect(me).toSucceed();
29
- expect(me.data.email).toEqual(EMAIL);
29
+ expect(me.body.email).toEqual(EMAIL);
30
30
 
31
31
  const logout = await api.post("/auth/logout");
32
32
  expect(logout).toSucceed();
@@ -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("=")[1],
39
+ "auth:" + login.headers["set-cookie"].split(";")[0].split("=")[1],
40
40
  ]);
41
41
  });
42
42
  });
@@ -18,7 +18,7 @@ describe("user creation flow", () => {
18
18
  .test();
19
19
 
20
20
  // Bun's bug: https://github.com/oven-sh/bun/issues/6348
21
- it.skip("tests a long user flow with tokens", async () => {
21
+ it("tests a long user flow with tokens", async () => {
22
22
  // The latest updated token
23
23
  let token;
24
24
 
@@ -28,8 +28,8 @@ describe("user creation flow", () => {
28
28
  expect(register).toSucceed();
29
29
 
30
30
  expect(await users()).toEqual(["abc@test.com"]);
31
- expect(await sessions()).toEqual([register.data.token]);
32
- return register.data.token;
31
+ expect(await sessions()).toEqual([register.body.token]);
32
+ return register.body.token;
33
33
  })();
34
34
 
35
35
  // CAN GET MY OWN INFO
@@ -37,7 +37,7 @@ describe("user creation flow", () => {
37
37
  const headers = { authorization: "Bearer " + token };
38
38
  const me = await api.get("/me", { headers });
39
39
  expect(me).toSucceed();
40
- expect(me.data.email).toEqual(EMAIL);
40
+ expect(me.body.email).toEqual(EMAIL);
41
41
 
42
42
  expect(await users()).toEqual(["abc@test.com"]);
43
43
  expect(await sessions()).toEqual([token]);
@@ -57,8 +57,8 @@ describe("user creation flow", () => {
57
57
  const login = await api.post("/auth/login/email", CREDENTIALS);
58
58
  expect(login).toSucceed();
59
59
  expect(await users()).toEqual(["abc@test.com"]);
60
- expect(await sessions()).toEqual([login.data.token]);
61
- return login.data.token;
60
+ expect(await sessions()).toEqual([login.body.token]);
61
+ return login.body.token;
62
62
  })();
63
63
 
64
64
  // CAN GET MY OWN INFO
@@ -66,7 +66,7 @@ describe("user creation flow", () => {
66
66
  const headers = { authorization: "Bearer " + token };
67
67
  const me = await api.get("/me", { headers });
68
68
  expect(me).toSucceed();
69
- expect(me.data.email).toEqual(EMAIL);
69
+ expect(me.body.email).toEqual(EMAIL);
70
70
  })();
71
71
 
72
72
  // UPDATE PASSWORD
@@ -103,8 +103,8 @@ describe("user creation flow", () => {
103
103
  });
104
104
  expect(login).toSucceed();
105
105
  expect(await users()).toEqual(["abc@test.com"]);
106
- expect(await sessions()).toEqual([login.data.token]);
107
- return login.data.token;
106
+ expect(await sessions()).toEqual([login.body.token]);
107
+ return login.body.token;
108
108
  })();
109
109
  });
110
110
  });
@@ -88,7 +88,7 @@ async function register(ctx) {
88
88
 
89
89
  const time = new Date().toISOString().replace(/\.[0-9]*/, "");
90
90
  const user = {
91
- id: createId(user.email),
91
+ id: createId(email),
92
92
  email,
93
93
  password: await hash.hash(password),
94
94
  time,
@@ -17,6 +17,17 @@ export default async (request, options = {}, app) => {
17
17
  ctx.res = { status: null, headers: {}, cookies: {} };
18
18
  ctx.method = request.method.toLowerCase();
19
19
 
20
+ // Private
21
+ const events = {};
22
+ ctx.unstableOn = (name, callback) => {
23
+ events[name] = events[name] || [];
24
+ events[name].push(callback);
25
+ };
26
+ ctx.unstableFire = (name, data) => {
27
+ if (!events[name]) return;
28
+ events[name].forEach((cb) => cb(data));
29
+ };
30
+
20
31
  ctx.headers = parseHeaders(new Headers(chunkArray(request.rawHeaders, 2)));
21
32
  ctx.cookies = parseCookies(ctx.headers.cookie);
22
33
  await auth.load(ctx);
@@ -26,7 +37,7 @@ export default async (request, options = {}, app) => {
26
37
  const path = request.url.replace(/\/$/, "") || "/";
27
38
  ctx.url = new URL(path, `${https}://${host}`);
28
39
  define(ctx.url, "query", (url) =>
29
- Object.fromEntries(url.searchParams.entries())
40
+ Object.fromEntries(url.searchParams.entries()),
30
41
  );
31
42
 
32
43
  await new Promise((resolve, reject) => {
@@ -40,7 +51,7 @@ export default async (request, options = {}, app) => {
40
51
  ctx.body = await parseBody(
41
52
  Buffer.concat(body).toString(),
42
53
  type,
43
- options.uploads
54
+ options.uploads,
44
55
  );
45
56
  resolve();
46
57
  })
@@ -5,18 +5,30 @@ import parseCookies from "./parseCookies.js";
5
5
 
6
6
  export default async (request, options = {}, app) => {
7
7
  const ctx = {};
8
+ ctx.init = performance.now();
8
9
  ctx.options = options;
9
10
  ctx.req = request;
10
11
  ctx.res = { status: null, headers: {}, cookies: {} };
11
12
  ctx.method = request.method.toLowerCase();
12
13
 
14
+ // Private
15
+ const events = {};
16
+ ctx.unstableOn = (name, callback) => {
17
+ events[name] = events[name] || [];
18
+ events[name].push(callback);
19
+ };
20
+ ctx.unstableFire = (name, data) => {
21
+ if (!events[name]) return;
22
+ events[name].forEach((cb) => cb(data));
23
+ };
24
+
13
25
  ctx.headers = parseHeaders(request.headers);
14
26
  ctx.cookies = parseCookies(ctx.headers.cookie);
15
27
  await auth.load(ctx);
16
28
 
17
29
  ctx.url = new URL(request.url.replace(/\/$/, ""));
18
30
  define(ctx.url, "query", (url) =>
19
- Object.fromEntries(url.searchParams.entries())
31
+ Object.fromEntries(url.searchParams.entries()),
20
32
  );
21
33
 
22
34
  if (request.body) {
@@ -3,7 +3,7 @@ import createId from "./createId.js";
3
3
 
4
4
  // Big mess; parse all of the options for server, which can be at launch time
5
5
  // or dynamically per-request for the functions (so have to read ENV inside)
6
- export default function config(options) {
6
+ export default function config(options = {}) {
7
7
  const env = globalThis.env;
8
8
 
9
9
  // Basic options
@@ -1,5 +1,9 @@
1
1
  const entities = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
2
- const encode = (str = "") => str.replace(/[&<>"]/g, (tag) => entities[tag]);
2
+ const encode = (str = "") => {
3
+ if (typeof str === "number") str = String(str);
4
+ if (typeof str !== "string") return ""; // nullify not-strings
5
+ return str.replace(/[&<>"]/g, (tag) => entities[tag]);
6
+ };
3
7
 
4
8
  const SELFCLOSE = new Set(
5
9
  "area,base,br,col,embed,hr,img,input,link,meta,source,track,wbr".split(","),
@@ -13,7 +17,7 @@ export const jsx = (tag, { children, ...props }) => {
13
17
  if (typeof tag === "function") return tag({ children, ...props });
14
18
 
15
19
  if (props.dangerouslySetInnerHTML)
16
- children = props.dangerouslySetInnerHTML.__html;
20
+ children = () => props.dangerouslySetInnerHTML.__html;
17
21
  if (!children) children = [];
18
22
  if (typeof children === "string") children = [children];
19
23
  children = (Array.isArray(children) ? children : [children])
@@ -22,12 +26,19 @@ export const jsx = (tag, { children, ...props }) => {
22
26
  .join("");
23
27
  if (!tag) return () => children;
24
28
  let attrStr = Object.entries(props || {})
29
+ .filter(([k, v]) => k !== "dangerouslySetInnerHTML")
25
30
  .filter(([k, v]) => !/on[A-Z]/.test(k) && typeof v !== "function")
26
- .map(([k, v]) => `${altAttrs[k.toLowerCase()] || encode(k)}="${encode(v)}"`)
31
+ .filter(([k, v]) => v !== false)
32
+ .map(([k, v]) =>
33
+ v === true
34
+ ? altAttrs[k.toLowerCase()] || encode(k)
35
+ : `${altAttrs[k.toLowerCase()] || encode(k)}="${encode(v)}"`,
36
+ )
27
37
  .join(" ");
28
38
  if (attrStr) attrStr = " " + attrStr;
29
39
  if (SELFCLOSE.has(tag)) return () => `<${tag}${attrStr} />`;
30
- return () => `<${tag}${attrStr}>${children}</${tag}>`;
40
+ const doctype = tag === "html" ? "<!DOCTYPE html>" : "";
41
+ return () => `${doctype}<${tag}${attrStr}>${children}</${tag}>`;
31
42
  };
32
43
  export const jsxDEV = jsx;
33
44
  export const Fragment = "";
@@ -0,0 +1,48 @@
1
+ import { jsx } from "./jsx.js";
2
+
3
+ const $ = (code) => code();
4
+
5
+ expect.extend({
6
+ toRender(fn, rendered) {
7
+ const html = fn();
8
+ if (this.isNot) {
9
+ const msg = `Expected "${html}" to render differently`;
10
+ return { pass: html === rendered, message: () => msg };
11
+ }
12
+ const msg = `Expected "${html}" to be rendered as "${rendered}"`;
13
+ return { pass: html === rendered, message: () => msg };
14
+ },
15
+ });
16
+
17
+ describe("jsx", () => {
18
+ it("can render a div", () => {
19
+ expect(<div>Hello</div>).toRender(`<div>Hello</div>`);
20
+ });
21
+
22
+ it("can render an input with attributes", () => {
23
+ expect(<input name="hello" />).toRender(`<input name="hello" />`);
24
+ });
25
+
26
+ it("will stringify an attribute", () => {
27
+ expect(<input value={5.2} />).toRender('<input value="5.2" />');
28
+ });
29
+
30
+ it("will ignore a boolean attribute", () => {
31
+ expect(<input disabled />).toRender("<input disabled />");
32
+ expect(<input disabled={true} />).toRender("<input disabled />");
33
+ expect(<input disabled={false} />).toRender("<input />");
34
+ });
35
+
36
+ it("will remove events", () => {
37
+ expect(<input onChange={() => console.log("hello")} />).toRender(
38
+ "<input />",
39
+ );
40
+ });
41
+
42
+ it("will inject the doctype for html", () => {
43
+ expect(<html>Hello</html>).toRender("<!DOCTYPE html><html>Hello</html>");
44
+ expect(<html lang="en">Hello</html>).toRender(
45
+ `<!DOCTYPE html><html lang="en">Hello</html>`,
46
+ );
47
+ });
48
+ });
package/src/index.js CHANGED
@@ -116,7 +116,7 @@ server.prototype.node = async function () {
116
116
  response.end();
117
117
  }
118
118
  })
119
- .listen(options.port);
119
+ .listen(this.opts.port);
120
120
  };
121
121
 
122
122
  // Netlify
@@ -139,12 +139,16 @@ server.prototype.fetch = async function (request, env) {
139
139
  if (env?.upgrade(request)) return;
140
140
  Object.assign(globalThis.env, env); // Extend env with the passed vars
141
141
 
142
+ let ctx, res, error;
142
143
  try {
143
- const ctx = await createWinterContext(request, this.opts, this);
144
- return await handleRequest(this.handlers, ctx);
145
- } catch (error) {
146
- return new Response(error.message, { status: error.status || 500 });
144
+ ctx = await createWinterContext(request, this.opts, this);
145
+ res = await handleRequest(this.handlers, ctx);
146
+ } catch (err) {
147
+ error = err;
148
+ res = new Response(error.message, { status: error.status || 500 });
147
149
  }
150
+ ctx?.unstableFire("finish", { ...ctx, error, res, end: performance.now() });
151
+ return res;
148
152
  };
149
153
 
150
154
  // #region HTTP methods
@@ -240,7 +244,7 @@ server.prototype.test = function () {
240
244
  } else {
241
245
  body = await res.text();
242
246
  }
243
- return { status: res.status, headers, body, data: body };
247
+ return { status: res.status, headers, body };
244
248
  };
245
249
  return {
246
250
  app: this,
package/src/index.test.js CHANGED
@@ -45,13 +45,13 @@ describe("return different types", () => {
45
45
  .test();
46
46
 
47
47
  it("can get the plain text", async () => {
48
- const { data } = await api.get("/text");
49
- expect(data).toBe("Hello world");
48
+ const { body } = await api.get("/text");
49
+ expect(body).toBe("Hello world");
50
50
  });
51
51
 
52
52
  it("can get the array", async () => {
53
- const { data } = await api.get("/array");
54
- expect(data).toEqual(["Hello world"]);
53
+ const { body } = await api.get("/array");
54
+ expect(body).toEqual(["Hello world"]);
55
55
  });
56
56
 
57
57
  it("can get the object", async () => {
@@ -71,19 +71,17 @@ describe("simple post works", () => {
71
71
  .post("/", (ctx) => status(201).send(ctx.body))
72
72
  .test();
73
73
 
74
- // Bun's bug: https://github.com/oven-sh/bun/issues/6348
75
- it.skip("can post new data", async () => {
76
- const { data, status, headers } = await api.post("/", "New Data");
74
+ it("can post new data", async () => {
75
+ const { body, status, headers } = await api.post("/", "New Data");
77
76
  expect(status).toBe(201);
78
- expect(data).toBe("New Data");
77
+ expect(body).toBe("New Data");
79
78
  expect(headers["content-type"]).toBe("text/plain; charset=utf-8");
80
79
  });
81
80
 
82
- // Bun's bug: https://github.com/oven-sh/bun/issues/6348
83
- it.skip("will return JSON", async () => {
84
- const { data, status, headers } = await api.post("/", { hello: "world" });
81
+ it("will return JSON", async () => {
82
+ const { body, status, headers } = await api.post("/", { hello: "world" });
85
83
  expect(status).toBe(201);
86
- expect(data).toEqual({ hello: "world" });
84
+ expect(body).toEqual({ hello: "world" });
87
85
  expect(headers["content-type"]).toBe("application/json; charset=utf-8");
88
86
  });
89
87
  });
@@ -2,7 +2,7 @@ import { type } from "../reply.js";
2
2
 
3
3
  export default async function assets(ctx) {
4
4
  if (!ctx.options.public) return;
5
- if (!ctx.method !== "get") return;
5
+ if (ctx.method !== "get") return;
6
6
  try {
7
7
  // TODO: streaming
8
8
  // Read it as buffer (null)
@@ -0,0 +1,10 @@
1
+ import server from "../";
2
+
3
+ describe("static assets", () => {
4
+ it("can serve a simple file", async () => {
5
+ const app = server({ public: "./src/middle/" }).test();
6
+ const { body, headers } = await app.get("/assets.test.js");
7
+ expect(body).toInclude("describe");
8
+ expect(headers["content-type"]).toBe("text/javascript");
9
+ });
10
+ });
@@ -20,27 +20,27 @@ describe("can route properly", () => {
20
20
 
21
21
  it("can get fallback when nothing matches", async () => {
22
22
  const res = await api.get("/");
23
- expect(res.data).toBe("Fallback");
23
+ expect(res.body).toBe("Fallback");
24
24
  });
25
25
 
26
26
  it("can get the base get", async () => {
27
27
  const res = await api.get("/hello");
28
- expect(res.data).toBe("Hello /hello");
28
+ expect(res.body).toBe("Hello /hello");
29
29
  });
30
30
 
31
31
  it("can get the nested get", async () => {
32
32
  const res = await api.get("/api/hello");
33
- expect(res.data).toBe("Hello /api/hello");
33
+ expect(res.body).toBe("Hello /api/hello");
34
34
  });
35
35
 
36
36
  it("can post to the base get", async () => {
37
37
  const res = await api.post("/hello");
38
- expect(res.data).toBe("Hello /hello");
38
+ expect(res.body).toBe("Hello /hello");
39
39
  });
40
40
 
41
41
  it("can post to the nested get", async () => {
42
42
  const res = await api.post("/api/hello");
43
- expect(res.data).toBe("Hello /api/hello");
43
+ expect(res.body).toBe("Hello /api/hello");
44
44
  });
45
45
 
46
46
  it("no status reuse", async () => {
@@ -20,22 +20,22 @@ describe("session", () => {
20
20
  await store.set("session:REqA2l022l8Q0tuIRtqLOPUy", { a: 0 });
21
21
 
22
22
  const res = await api.get("/hello", options);
23
- expect(res.data).toBe("Hello 0");
23
+ expect(res.body).toBe("Hello 0");
24
24
 
25
25
  const res2 = await api.post("/hello", {}, options);
26
- expect(res2.data).toBe("Bye 1");
26
+ expect(res2.body).toBe("Bye 1");
27
27
 
28
28
  const res3 = await api.get("/hello", options);
29
- expect(res3.data).toBe("Hello 1");
29
+ expect(res3.body).toBe("Hello 1");
30
30
  expect(await store.get("session:REqA2l022l8Q0tuIRtqLOPUy")).toEqual({
31
31
  a: 1,
32
32
  });
33
33
 
34
34
  const res4 = await api.post("/hello", {}, options);
35
- expect(res4.data).toBe("Bye 2");
35
+ expect(res4.body).toBe("Bye 2");
36
36
 
37
37
  const res5 = await api.get("/hello", options);
38
- expect(res5.data).toBe("Hello 2");
38
+ expect(res5.body).toBe("Hello 2");
39
39
  expect(await store.get("session:REqA2l022l8Q0tuIRtqLOPUy")).toEqual({
40
40
  a: 2,
41
41
  });
@@ -54,12 +54,12 @@ describe("missing store", () => {
54
54
  it("cannot read a session without a store", async () => {
55
55
  const res = await api.get("/read");
56
56
  expect(res.status).toBe(500);
57
- expect(res.data).toBe("You need a 'store' to read 'ctx.session.a'");
57
+ expect(res.body).toBe("You need a 'store' to read 'ctx.session.a'");
58
58
  });
59
59
 
60
60
  it("cannot write a session without a store", async () => {
61
61
  const res = await api.get("/write");
62
62
  expect(res.status).toBe(500);
63
- expect(res.data).toBe("You need a 'store' to write 'ctx.session.a'");
63
+ expect(res.body).toBe("You need a 'store' to write 'ctx.session.a'");
64
64
  });
65
65
  });
@@ -10,17 +10,17 @@ const spaceOrEnter = (msg) => {
10
10
  };
11
11
 
12
12
  export default function toSucceed(request, message) {
13
- const pass = request.status >= 200 && request.status < 300;
13
+ const pass = request.status >= 200 && request.status < 400;
14
14
 
15
- if (message && JSON.stringify(request.data) !== JSON.stringify(message)) {
15
+ if (message && JSON.stringify(request.body) !== JSON.stringify(message)) {
16
16
  if (pass) {
17
17
  return {
18
18
  message: () =>
19
19
  `${reset}Expected body:${spaceOrEnter(
20
- message
20
+ message,
21
21
  )}${this.utils.printExpected(message)}\nReceived body:${spaceOrEnter(
22
- request.data
23
- )}${this.utils.printReceived(request.data)}`,
22
+ request.body,
23
+ )}${this.utils.printReceived(request.body)}`,
24
24
  pass,
25
25
  };
26
26
  }
@@ -28,10 +28,10 @@ export default function toSucceed(request, message) {
28
28
  return {
29
29
  message: () =>
30
30
  `${reset}Expected error:${spaceOrEnter(
31
- message
31
+ message,
32
32
  )}${this.utils.printExpected(message)}\nReceived error:${spaceOrEnter(
33
- request.data
34
- )}${this.utils.printReceived(request.data)}`,
33
+ request.body,
34
+ )}${this.utils.printReceived(request.body)}`,
35
35
  pass: !pass,
36
36
  };
37
37
  }
@@ -40,9 +40,9 @@ export default function toSucceed(request, message) {
40
40
  return {
41
41
  message: () =>
42
42
  `${reset}Expected ${this.utils.printExpected(
43
- request.status
43
+ request.status,
44
44
  )} to be an error code, received body:\n${this.utils.printExpected(
45
- request.data
45
+ request.body,
46
46
  )}`,
47
47
  pass: true,
48
48
  };
@@ -51,11 +51,11 @@ export default function toSucceed(request, message) {
51
51
  return {
52
52
  message: () =>
53
53
  `${reset}Expected ${this.utils.printReceived(
54
- request.status
54
+ request.status,
55
55
  )} to succeed, received body:\n${this.utils.printReceived(
56
- request.data,
56
+ request.body,
57
57
  null,
58
- 2
58
+ 2,
59
59
  )}`,
60
60
  pass: false,
61
61
  };
package/src/url.test.js CHANGED
@@ -7,8 +7,8 @@ describe("can match the url", () => {
7
7
  .get("/*", (ctx) => ctx.url.params)
8
8
  .test();
9
9
 
10
- const { data, headers } = await api.get("/hello");
11
- expect(data).toEqual({ id: "hello" });
10
+ const { body, headers } = await api.get("/hello");
11
+ expect(body).toEqual({ id: "hello" });
12
12
  expect(headers["content-type"]).toEqual("application/json; charset=utf-8");
13
13
  });
14
14
 
@@ -19,8 +19,8 @@ describe("can match the url", () => {
19
19
  .get("/*", (ctx) => ctx.url.params)
20
20
  .test();
21
21
 
22
- const { data, headers, ...rest } = await api.get("/hello");
23
- expect(data).toEqual({ id: "hello" });
22
+ const { body, headers, ...rest } = await api.get("/hello");
23
+ expect(body).toEqual({ id: "hello" });
24
24
  expect(headers["content-type"]).toEqual("application/json; charset=utf-8");
25
25
  });
26
26
  });