@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 +34 -1
- package/package.json +7 -7
- package/readme.md +111 -0
- package/src/helpers/getMachine.js +12 -1
- package/src/helpers/handleRequest.js +6 -0
- package/src/pathPattern.js +1 -1
- package/src/pathPattern.test.js +5 -3
package/index.d.ts
CHANGED
|
@@ -27,7 +27,37 @@ type Context = {
|
|
|
27
27
|
options: ServerOptions;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
type
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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
|
}
|
package/src/pathPattern.js
CHANGED
package/src/pathPattern.test.js
CHANGED
|
@@ -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(
|
|
23
|
-
expect(pathPattern("/hello/", "/hello/John")).toEqual(
|
|
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(
|
|
36
|
+
expect(pathPattern("/hello/:there/*", "/hello/John")).toEqual(null);
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
it("can make a part optional", () => {
|