@server/next 0.20.32 → 0.20.33

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/index.d.ts CHANGED
@@ -27,7 +27,37 @@ type Context = {
27
27
  options: ServerOptions;
28
28
  };
29
29
 
30
- type Middleware = (ctx: Context) => any;
30
+ type Body = string;
31
+
32
+ type ContentType = "application/json" | "text/plain" | (string & {});
33
+
34
+ // (src: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
35
+ type Headers = {
36
+ "Cache-Control"?: string;
37
+ "Content-Type"?: ContentType;
38
+ Server?: string;
39
+ "Set-Cookie"?: string;
40
+ "Content-Length"?: string;
41
+ Location?: string;
42
+
43
+ "cache-control"?: string;
44
+ "content-type"?: ContentType;
45
+ server?: string;
46
+ "set-cookie"?: string;
47
+ "content-length"?: string;
48
+ location?: string;
49
+
50
+ [key: string]: string | undefined;
51
+ };
52
+
53
+ type InlineReply =
54
+ | Response
55
+ | { body: Body; headers?: Headers }
56
+ | string
57
+ | number
58
+ | void;
59
+
60
+ type Middleware = (ctx: Context) => InlineReply;
31
61
 
32
62
  type Router = {};
33
63
 
@@ -47,5 +77,8 @@ declare interface Server {
47
77
  router(router: Router): this;
48
78
  }
49
79
 
80
+ type headers = (obj?: Headers) => any;
81
+
50
82
  declare const server: Server;
83
+ export const headers: headers;
51
84
  export default server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.20.32",
3
+ "version": "0.20.33",
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",
@@ -25,18 +25,18 @@
25
25
  "src/",
26
26
  "index.d.ts"
27
27
  ],
28
- "engines": {
29
- "node": ">=20.0.0"
30
- },
31
- "engineStrict": true,
28
+ "dependencies": {},
32
29
  "devDependencies": {
33
30
  "argon2": "^0.40.3",
34
31
  "jest": "^29.7.0",
35
32
  "polystore": "^0.8.0"
36
33
  },
34
+ "engines": {
35
+ "node": ">=20.0.0"
36
+ },
37
+ "engineStrict": true,
37
38
  "jest": {
38
39
  "testEnvironment": "jest-environment-node",
39
40
  "transform": {}
40
- },
41
- "dependencies": {}
41
+ }
42
42
  }
package/readme.md CHANGED
@@ -104,6 +104,117 @@ export default server({ bucket, store })
104
104
 
105
105
  ### File handling
106
106
 
107
+ To manage files, you need to install and use the library [`bucket`](http://bucketjs.com/), which is a very thin wrapper for file management systems. It is also created by the makers of Server.js.
108
+
109
+ The easiest and default is to set a folder in your filesystem:
110
+
111
+ ```js
112
+ import FileSystem from "bucket/fs";
113
+
114
+ const uploads = FileSystem("./uploads");
115
+
116
+ // All paths are relative to the CWD
117
+ export default server({ uploads })
118
+ .get("/", () => "Hello")
119
+ .put("/users/:id", async (ctx) => {
120
+ // This is the plain string as the file name, already in our FS
121
+ const fileName = ctx.body.profile;
122
+ // 'yOuZEdSsNLq8PgZyLhSz0Llh.jpg'
123
+
124
+ // Convert it into a File instance
125
+ const file = ctx.uploads.file(fileName);
126
+
127
+ // Now we can use other methods if we want
128
+ // .info(), .read(), .write(), .pipe(), .pipeTo(), etc
129
+ const info = await file.info();
130
+ // {
131
+ // id: "yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
132
+ // type: "jpg",
133
+ // size: 435435,
134
+ // timestamp: "2024-08-07T14:26:37Z",
135
+ // // Note: this can be customized providing the option "domain"
136
+ // url: "file:///Users/me/my-project/uploads/yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
137
+ // }
138
+
139
+ return 200;
140
+ });
141
+ ```
142
+
143
+ To upload the files to a 3rd party system, you just need to use the corresponding `bucket` implementation (or write a thin compatibility layer). Let's see an example with Backblaze's B2:
144
+
145
+ ```js
146
+ import server from "@server/next";
147
+ import Backblaze from "bucket/b2";
148
+
149
+ const uploads = Backblaze("bucket-name", {
150
+ id: process.env.BACKBLAZE_ID,
151
+ key: process.env.BACKBLAZE_KEY,
152
+ });
153
+
154
+ export default server({ uploads })
155
+ .put("/users/:id", async (ctx) => {
156
+ const fileName = ctx.body.profile;
157
+ // 'yOuZEdSsNLq8PgZyLhSz0Llh.jpg'
158
+
159
+ // Convert it into a File instance
160
+ const file = ctx.uploads.file(fileName);
161
+
162
+ // Now we can use other methods if we want
163
+ const info = await file.info();
164
+ // {
165
+ // id: "yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
166
+ // type: "jpg",
167
+ // size: 435435,
168
+ // timestamp: "2024-08-07T14:26:37Z",
169
+ // url: "https://f???.backblazeb2.com/???/yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
170
+ // }
171
+
172
+ return 200;
173
+ });
174
+ .;
175
+ ```
176
+
177
+ #### Example: resizing the user profile picture
178
+
179
+ Let's see a complete example of uploading and resizing a user profile picture:
180
+
181
+ ```js
182
+ import server, { status } from "@server/next";
183
+ import sharp from "sharp";
184
+ import FileSystem from "bucket/fs";
185
+
186
+ const uploads = FileSystem("./uploads");
187
+
188
+ // All paths are relative to the CWD
189
+ export default server({ uploads })
190
+ .get("/", () => "Hello")
191
+ .put("/users/:id", async (ctx) => {
192
+ // Create the instance of the file to read and write
193
+ const src = ctx.uploads.file(ctx.body.profile);
194
+ const dst = ctx.uploads.file("/profile/" + ctx.url.params.id + ".jpg");
195
+
196
+ const ext = src.id.split(".").pop();
197
+ if (!["jpg", "jpeg", "png", "webp", "avif"].includes(ext)) {
198
+ await src.remove(); // Don't store it
199
+ return status(400).json({ error: "Invalid file format" });
200
+ }
201
+
202
+ // Create the Readable, Transform and Writable Node streams
203
+ await pipeline(
204
+ src.readable("node"),
205
+ sharp().resize(200, 200).jpg(),
206
+ dst.writable("node")
207
+ );
208
+
209
+ // We no longer need the original file
210
+ await src.remove();
211
+
212
+ return status(200).json({ updated: true });
213
+ });
214
+ ```
215
+
216
+ Note that the option `uploads` gets converted into a `Bucket` instance and passed as `ctx.uploads`. Th
217
+
107
218
  ## Options
108
219
 
109
220
  Options docs here
@@ -1,12 +1,23 @@
1
+ function getProvider() {
2
+ if ("Netlify" in globalThis) return "netlify";
3
+ return null;
4
+ }
5
+
6
+ function getService() {
7
+ return null;
8
+ }
9
+
1
10
  function getRuntime() {
2
11
  if ("Bun" in globalThis) return "bun";
3
12
  if ("Deno" in globalThis) return "deno";
4
13
  if (globalThis.process?.versions?.node) return "node";
5
- return "unknown";
14
+ return null;
6
15
  }
7
16
 
8
17
  export default function getMachine() {
9
18
  return {
19
+ provider: getProvider(),
20
+ service: getService(),
10
21
  runtime: getRuntime(),
11
22
  production: process.env.NODE_ENV === "production",
12
23
  };
@@ -5,6 +5,7 @@ import define from "./define.js";
5
5
  import validate from "./validate.js";
6
6
 
7
7
  const extendWithDefaults = (ctx) => {
8
+ // TODO: find a better way of doing this FFS
8
9
  // Only want to execute it once; it needs to happen on a per-request
9
10
  // basis since we only have full access to the options there
10
11
  if (ctx.app.extended) return;
@@ -36,5 +37,10 @@ export default async function handleRequest(handlers, ctx) {
36
37
  if (method !== "*") break;
37
38
  }
38
39
 
40
+ // In Netlify, a non-response is perfectly valid, which would indicate
41
+ // the edge function to just go ahead and consume the original resource
42
+ if (ctx.machine.provider === "netlify") return;
43
+
44
+ // In other environments, a non-response is wrong and we should 404 then
39
45
  return new Response("Not Found", { status: 404 });
40
46
  }
@@ -30,5 +30,5 @@ export default function pathPattern(pattern, path) {
30
30
  allSame = false;
31
31
  }
32
32
  if (allSame) return params;
33
- return false;
33
+ return null;
34
34
  }
@@ -2,6 +2,8 @@ import pathPattern from "./pathPattern.js";
2
2
 
3
3
  describe("pathPattern.js", () => {
4
4
  it("matches the same string", () => {
5
+ expect(pathPattern("/", "/hello")).toEqual(null);
6
+ expect(pathPattern("/hello", "/")).toEqual(null);
5
7
  expect(pathPattern("/hello", "/hello")).toEqual({});
6
8
  expect(pathPattern("/hello/world", "/hello/world")).toEqual({});
7
9
  });
@@ -19,8 +21,8 @@ describe("pathPattern.js", () => {
19
21
  });
20
22
 
21
23
  it("doesn't do partial matches", () => {
22
- expect(pathPattern("/hello", "/hello/John")).toEqual(false);
23
- expect(pathPattern("/hello/", "/hello/John")).toEqual(false);
24
+ expect(pathPattern("/hello", "/hello/John")).toEqual(null);
25
+ expect(pathPattern("/hello/", "/hello/John")).toEqual(null);
24
26
  });
25
27
 
26
28
  it("can capture simple groups", () => {
@@ -31,7 +33,7 @@ describe("pathPattern.js", () => {
31
33
  });
32
34
 
33
35
  it("requires a part for the asterisk", () => {
34
- expect(pathPattern("/hello/:there/*", "/hello/John")).toEqual(false);
36
+ expect(pathPattern("/hello/:there/*", "/hello/John")).toEqual(null);
35
37
  });
36
38
 
37
39
  it("can make a part optional", () => {