@server/next 0.25.9 → 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.9",
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",
@@ -33,9 +33,14 @@ function thinLocalBucket(root) {
33
33
  }
34
34
  return fs.createWriteStream(fullPath);
35
35
  },
36
- delete: (name) => {
36
+ delete: async (name) => {
37
37
  const fullPath = absolute(name);
38
- return fsp.unlink(fullPath);
38
+ try {
39
+ await fsp.unlink(fullPath);
40
+ return true;
41
+ } catch (error) {
42
+ return false;
43
+ }
39
44
  },
40
45
  };
41
46
  }
@@ -37,4 +37,15 @@ describe("bucket", () => {
37
37
  const stream = await localBucket.read("testFile.txt");
38
38
  expect(stream).toBeNull();
39
39
  });
40
+
41
+ it("removes an existing file", async () => {
42
+ await localBucket.write("testFile.txt", "To be deleted");
43
+ const isDeleted = await localBucket.delete("testFile.txt");
44
+ expect(isDeleted).toBe(true);
45
+ });
46
+
47
+ it("tries to remove a non-existing file", async () => {
48
+ const isDeleted = await localBucket.delete("nonExistentFile.txt");
49
+ expect(isDeleted).toBe(false);
50
+ });
40
51
  });
@@ -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.d.ts CHANGED
@@ -17,7 +17,21 @@ type Store = {
17
17
  ) => Promise<any>;
18
18
  };
19
19
 
20
- type Bucket = any;
20
+ type Bucket =
21
+ | string
22
+ | {
23
+ read: (key: string) => ReadableStream;
24
+ write: (
25
+ key: string,
26
+ data:
27
+ | string
28
+ | NodeJS.ArrayBufferView
29
+ | Iterable<string | NodeJS.ArrayBufferView>
30
+ | AsyncIterable<string | NodeJS.ArrayBufferView>,
31
+ type: BufferEncoding,
32
+ ) => Promise<Boolean>;
33
+ delete: (key: string) => Promise<Boolean>;
34
+ };
21
35
 
22
36
  type Auth = {
23
37
  type: "cookie" | "token";
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