@server/next 0.25.10 → 0.26.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.25.10",
3
+ "version": "0.26.0",
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",
@@ -32,8 +32,7 @@
32
32
  "main": "src/index.js",
33
33
  "types": "src/index.d.ts",
34
34
  "files": [
35
- "src/",
36
- "jsx-dev-runtime.js"
35
+ "src/"
37
36
  ],
38
37
  "exports": {
39
38
  ".": "./src/index.js",
@@ -0,0 +1,21 @@
1
+ export default function createWebsocket(sockets, handlers) {
2
+ return {
3
+ message: async (socket, body) => {
4
+ handlers.socket
5
+ ?.filter((s) => s[1] === "message")
6
+ ?.map((s) => s[2]({ socket, sockets, body }));
7
+ },
8
+ open: (socket) => {
9
+ sockets.push(socket);
10
+ handlers.socket
11
+ ?.filter((s) => s[1] === "open")
12
+ ?.map((s) => s[2]({ socket, sockets, body }));
13
+ },
14
+ close: (socket) => {
15
+ sockets.splice(sockets.indexOf(socket), 1);
16
+ handlers.socket
17
+ ?.filter((s) => s[1] === "close")
18
+ ?.map((s) => s[2]({ socket, sockets, body }));
19
+ },
20
+ };
21
+ }
@@ -1,11 +1,14 @@
1
1
  export { default as createCookies } from "./createCookies.js";
2
2
  export { default as createId } from "./createId.js";
3
+ export { default as createWebsocket } from "./createWebsocket.js";
3
4
  export { default as config } from "./config.js";
4
5
  export { default as cors } from "./cors.js";
5
6
  export { default as define } from "./define.js";
6
7
  export { default as getMachine } from "./getMachine.js";
7
8
  export { default as handleRequest } from "./handleRequest.js";
8
9
  export { default as iterate } from "./iterate.js";
10
+ export { default as iteratorToReadable } from "./iteratorToReadable.js";
11
+ export { default as iteratorAsyncToReadable } from "./iteratorAsyncToReadable.js";
9
12
  export { default as parseHeaders } from "./parseHeaders.js";
10
13
  export { default as toWeb } from "./toWeb.js";
11
14
  export { default as types } from "./types.js";
@@ -0,0 +1,20 @@
1
+ export default function iteratorAsyncToReadable(asyncGenerator) {
2
+ return new ReadableStream({
3
+ async pull(controller) {
4
+ try {
5
+ const { value, done } = await asyncGenerator.next();
6
+ if (done) {
7
+ controller.close();
8
+ return;
9
+ }
10
+ controller.enqueue(new TextEncoder().encode(value));
11
+ } catch (err) {
12
+ console.error("Stream error:", err);
13
+ controller.error(err);
14
+ }
15
+ },
16
+ cancel() {
17
+ console.log("Stream cancelled");
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,10 @@
1
+ export default function iteratorToReadable(generator) {
2
+ return new ReadableStream({
3
+ async start(controller) {
4
+ for await (const chunk of generator) {
5
+ controller.enqueue(chunk);
6
+ }
7
+ controller.close();
8
+ },
9
+ });
10
+ }
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import createNodeContext from "./context/node.js";
5
5
  import createWinterContext from "./context/winter.js";
6
6
  import {
7
7
  config,
8
+ createWebsocket,
8
9
  getMachine,
9
10
  handleRequest,
10
11
  iterate,
@@ -31,9 +32,6 @@ export default function server(options = {}) {
31
32
  this.opts = config(options);
32
33
  this.platform = getMachine();
33
34
 
34
- // TODO: find a way to remove this hack
35
- this.extended = false;
36
-
37
35
  // Skip "forbidden methods" https://fetch.spec.whatwg.org/#concept-method
38
36
  this.handlers = {
39
37
  socket: [],
@@ -46,27 +44,11 @@ export default function server(options = {}) {
46
44
  options: [],
47
45
  };
48
46
 
47
+ // Keep a reference of the currently connected sockets
49
48
  this.sockets = [];
49
+
50
50
  // Note: required by Bun
51
- this.websocket = {
52
- message: async (socket, body) => {
53
- this.handlers.socket
54
- ?.filter((s) => s[1] === "message")
55
- ?.map((s) => s[2]({ socket, sockets: this.sockets, body }));
56
- },
57
- open: (ws) => {
58
- this.sockets.push(ws);
59
- this.handlers.socket
60
- ?.filter((s) => s[1] === "open")
61
- ?.map((s) => s[2]({ socket, sockets: this.sockets, body }));
62
- },
63
- close: (ws) => {
64
- this.sockets.splice(this.sockets.indexOf(ws), 1);
65
- this.handlers.socket
66
- ?.filter((s) => s[1] === "close")
67
- ?.map((s) => s[2]({ socket, sockets: this.sockets, body }));
68
- },
69
- };
51
+ this.websocket = createWebsocket(this.sockets, this.handlers);
70
52
 
71
53
  // Initialize it right away for Node.js
72
54
  if (this.platform.runtime === "node") {
@@ -1,4 +1,9 @@
1
- import { cors, createId } from "./helpers/index.js";
1
+ import {
2
+ cors,
3
+ createId,
4
+ iteratorToReadable,
5
+ iteratorAsyncToReadable,
6
+ } from "./helpers/index.js";
2
7
  import { json } from "./reply.js";
3
8
  import ServerError from "./ServerError.js";
4
9
 
@@ -34,14 +39,25 @@ export default async function parseResponse(out, ctx) {
34
39
  out = json(out);
35
40
  }
36
41
 
42
+ // Sync and Async iterators
43
+ if (out[Symbol.iterator]) {
44
+ out = new Response(iteratorToReadable(out));
45
+ }
46
+
47
+ // The ReadableStream seems to be an asyncIterator, but we don't want to handle that yet
48
+ if (out[Symbol.asyncIterator] && !(out instanceof Response)) {
49
+ out = new Response(iteratorAsyncToReadable(out));
50
+ }
51
+
37
52
  // The output from fetch(), create a copy of it into a new response
38
53
  if (out instanceof Response && out.url && out.body) {
39
54
  out = new Response(out.body, {
40
- status: 200,
55
+ status: out.status,
41
56
  headers: out.headers,
42
57
  });
58
+
59
+ // Compression not supported for streaming response, stripping header
43
60
  if (/^(br|gzip)$/.test(out.headers.get("content-encoding"))) {
44
- console.warn("Compression not yet supported for response");
45
61
  out.headers.delete("content-encoding");
46
62
  }
47
63
  }
package/src/reply.js CHANGED
@@ -76,11 +76,11 @@ Reply.prototype.redirect = function (Location) {
76
76
  return this.headers({ Location }).status(302).send();
77
77
  };
78
78
 
79
- Reply.prototype.file = async function (path) {
79
+ Reply.prototype.file = async function (path, renderer = (data) => data) {
80
80
  try {
81
81
  const data = await fs.readFile(path);
82
82
  const ext = path.split(".").pop();
83
- return this.type(ext).send(data);
83
+ return this.type(ext).send(await renderer(data));
84
84
  } catch (error) {
85
85
  if (error.code === "ENOENT") {
86
86
  return status(404).send();
@@ -89,14 +89,14 @@ Reply.prototype.file = async function (path) {
89
89
  }
90
90
  };
91
91
 
92
- Reply.prototype.view = async function (path) {
92
+ Reply.prototype.view = async function (path, renderer = (data) => data) {
93
93
  return async (ctx) => {
94
94
  if (!ctx.options.views) {
95
95
  throw new Error("Views not enabled");
96
96
  }
97
97
  const data = await ctx.options.views.read(path);
98
- if (data) return this.type(path.split(".").pop()).send(data);
99
- return this.status(404).send();
98
+ if (!data) return this.status(404).send();
99
+ return this.type(path.split(".").pop()).send(await renderer(data));
100
100
  };
101
101
  };
102
102