@server/next 0.20.31 → 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 +9 -9
- package/readme.md +154 -0
- package/src/helpers/config.js +68 -0
- package/src/helpers/getMachine.js +12 -1
- package/src/helpers/handleRequest.js +17 -0
- package/src/helpers/index.js +1 -0
- package/src/index.js +81 -143
- package/src/index.test.js +15 -0
- package/src/pathPattern.js +1 -1
- package/src/pathPattern.test.js +5 -3
- package/src/polyfill.js +9 -3
- /package/src/{bucket.js → helpers/bucket.js} +0 -0
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.20.
|
|
4
|
-
"description": "
|
|
5
|
-
"homepage": "https://
|
|
3
|
+
"version": "0.20.33",
|
|
4
|
+
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
|
+
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "https://github.com/franciscop/server-next.git",
|
|
7
7
|
"bugs": "https://github.com/franciscop/server-next/issues",
|
|
8
8
|
"funding": "https://www.paypal.me/franciscopresencia/19",
|
|
@@ -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
|
|
@@ -119,3 +230,46 @@ Context docs here
|
|
|
119
230
|
## Reply
|
|
120
231
|
|
|
121
232
|
Reply docs here
|
|
233
|
+
|
|
234
|
+
## Runtimes
|
|
235
|
+
|
|
236
|
+
There are many runtimes where Server works! We put a lot of work to make sure it works the same way with minimal changes in them, this includes:
|
|
237
|
+
|
|
238
|
+
- Node.js of course, including everywhere that Node is supported (VPS, Heroku, Render, etc).
|
|
239
|
+
- Bun, and it will be even faster!
|
|
240
|
+
- Cloudflare Worker
|
|
241
|
+
- Netlify Function + Edge function
|
|
242
|
+
|
|
243
|
+
## FAQ
|
|
244
|
+
|
|
245
|
+
#### How is it different from Hono?
|
|
246
|
+
|
|
247
|
+
Server.js attempts to run your code unmodified in all runtimes. With Hono, despite the claims in their homepage, you need to change the code for different runtimes:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// Server.js code for Node.js, Bun and Netlify
|
|
251
|
+
import server from "@server/next";
|
|
252
|
+
export default server().get("/", () => "Hello server!");
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
// Hono code for Node.js
|
|
257
|
+
import { serve } from '@hono/node-server'
|
|
258
|
+
import { Hono } from 'hono'
|
|
259
|
+
const app = new Hono()
|
|
260
|
+
app.get('/', (c) => c.text('Hello Node.js!'))
|
|
261
|
+
serve(app)
|
|
262
|
+
|
|
263
|
+
// Hono code for Bun
|
|
264
|
+
import { Hono } from 'hono'
|
|
265
|
+
const app = new Hono()
|
|
266
|
+
app.get('/', (c) => c.text('Hello Bun!'))
|
|
267
|
+
export default app
|
|
268
|
+
|
|
269
|
+
// Hono code for Netlify
|
|
270
|
+
import { Hono } from 'jsr:@hono/hono'
|
|
271
|
+
import { handle } from 'jsr:@hono/hono/netlify'
|
|
272
|
+
const app = new Hono()
|
|
273
|
+
app.get('/', (c) => c.text('Hello Hono!'))
|
|
274
|
+
export default handle(app)
|
|
275
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Bucket from "./bucket.js";
|
|
2
|
+
import createId from "./createId.js";
|
|
3
|
+
|
|
4
|
+
// Big mess; parse all of the options for server, which can be at launch time
|
|
5
|
+
// or dynamically per-request for the functions (so have to read ENV inside)
|
|
6
|
+
export default function config(options) {
|
|
7
|
+
const env = globalThis.env;
|
|
8
|
+
|
|
9
|
+
// Basic options
|
|
10
|
+
options.port = options.port || env.PORT || 3000;
|
|
11
|
+
options.secret = options.secret || env.SECRET || "unsafe-" + createId();
|
|
12
|
+
|
|
13
|
+
// CORS
|
|
14
|
+
options.cors = options.cors || env.CORS || null;
|
|
15
|
+
if (options.cors === true) {
|
|
16
|
+
options.cors = { origin: options.domain || "*" };
|
|
17
|
+
}
|
|
18
|
+
if (typeof options.cors === "string") {
|
|
19
|
+
options.cors = { origin: options.cors };
|
|
20
|
+
}
|
|
21
|
+
if (options.cors && !options.cors.methods) {
|
|
22
|
+
options.cors.methods = "GET,HEAD,POST,PUT,PATCH";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Bucket
|
|
26
|
+
options.views = options.views ? Bucket(options.views) : null;
|
|
27
|
+
options.public = options.public ? Bucket(options.public) : null;
|
|
28
|
+
options.uploads = options.uploads ? Bucket(options.uploads) : null;
|
|
29
|
+
|
|
30
|
+
// Stores
|
|
31
|
+
options.store = options.store ?? null;
|
|
32
|
+
options.cookies = options.cookies ?? {};
|
|
33
|
+
if (options.store && options.cookies) {
|
|
34
|
+
options.session = { store: options.store.prefix("session:") };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// AUTH
|
|
38
|
+
options.auth = options.auth || env.AUTH || null;
|
|
39
|
+
if (options.auth) {
|
|
40
|
+
if (typeof options.auth !== "object") {
|
|
41
|
+
const [type, provider] = options.auth.split(":");
|
|
42
|
+
options.auth = { type, provider };
|
|
43
|
+
}
|
|
44
|
+
if (typeof options.auth.provider === "string") {
|
|
45
|
+
options.auth.provider === options.auth.provider.split("|");
|
|
46
|
+
}
|
|
47
|
+
if (!options.auth.type) {
|
|
48
|
+
throw new Error("Auth options needs a type");
|
|
49
|
+
}
|
|
50
|
+
if (!options.auth.provider) {
|
|
51
|
+
throw new Error("Auth options needs a provider");
|
|
52
|
+
}
|
|
53
|
+
if (!options.auth.session && options.store) {
|
|
54
|
+
options.auth.session = options.store.prefix("auth:");
|
|
55
|
+
}
|
|
56
|
+
if (!options.auth.store && options.store) {
|
|
57
|
+
options.auth.store = options.store.prefix("user:");
|
|
58
|
+
}
|
|
59
|
+
if (!options.auth.cleanUser) {
|
|
60
|
+
options.auth.cleanUser = (fullUser) => {
|
|
61
|
+
const { password, ...user } = fullUser;
|
|
62
|
+
return user;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
@@ -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
|
};
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
import middle from "../middle/index.js";
|
|
1
2
|
import parseResponse from "../parseResponse.js";
|
|
2
3
|
import pathPattern from "../pathPattern.js";
|
|
3
4
|
import define from "./define.js";
|
|
4
5
|
import validate from "./validate.js";
|
|
5
6
|
|
|
7
|
+
const extendWithDefaults = (ctx) => {
|
|
8
|
+
// TODO: find a better way of doing this FFS
|
|
9
|
+
// Only want to execute it once; it needs to happen on a per-request
|
|
10
|
+
// basis since we only have full access to the options there
|
|
11
|
+
if (ctx.app.extended) return;
|
|
12
|
+
middle(ctx);
|
|
13
|
+
ctx.app.extended = true;
|
|
14
|
+
};
|
|
15
|
+
|
|
6
16
|
export default async function handleRequest(handlers, ctx) {
|
|
17
|
+
extendWithDefaults(ctx);
|
|
18
|
+
|
|
7
19
|
for (let [method, matcher, ...cbs] of handlers[ctx.method]) {
|
|
8
20
|
const match = pathPattern(matcher, ctx.url.pathname || "/");
|
|
9
21
|
// Skip this whole middleware if there was no match
|
|
@@ -25,5 +37,10 @@ export default async function handleRequest(handlers, ctx) {
|
|
|
25
37
|
if (method !== "*") break;
|
|
26
38
|
}
|
|
27
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
|
|
28
45
|
return new Response("Not Found", { status: 404 });
|
|
29
46
|
}
|
package/src/helpers/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as createCookies } from "./createCookies.js";
|
|
2
2
|
export { default as createId } from "./createId.js";
|
|
3
|
+
export { default as config } from "./config.js";
|
|
3
4
|
export { default as define } from "./define.js";
|
|
4
5
|
export { default as getMachine } from "./getMachine.js";
|
|
5
6
|
export { default as handleRequest } from "./handleRequest.js";
|
package/src/index.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import "./polyfill.js";
|
|
2
|
-
// Define the errors for ServerError
|
|
3
2
|
import "./errors/index.js";
|
|
4
3
|
|
|
5
|
-
import Bucket from "./bucket.js";
|
|
6
4
|
import createNodeContext from "./context/node.js";
|
|
7
5
|
import createWinterContext from "./context/winter.js";
|
|
8
6
|
import {
|
|
9
|
-
|
|
7
|
+
config,
|
|
10
8
|
getMachine,
|
|
11
9
|
handleRequest,
|
|
12
10
|
iterate,
|
|
13
11
|
parseHeaders,
|
|
14
12
|
} from "./helpers/index.js";
|
|
15
|
-
import middle from "./middle/index.js";
|
|
16
13
|
|
|
17
14
|
// Export the reply helpers
|
|
18
15
|
export * from "./reply.js";
|
|
@@ -22,103 +19,20 @@ export { default as ServerError } from "./ServerError.js";
|
|
|
22
19
|
// Allow to create a sub-router
|
|
23
20
|
export { default as router } from "./router.js";
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
const http = await import("http");
|
|
27
|
-
http
|
|
28
|
-
.createServer(async (request, response) => {
|
|
29
|
-
try {
|
|
30
|
-
const ctx = await createNodeContext(request, options, app);
|
|
31
|
-
extendWithDefaults(ctx);
|
|
32
|
-
const out = await handleRequest(app.handlers, ctx);
|
|
33
|
-
|
|
34
|
-
response.writeHead(out.status || 200, parseHeaders(out.headers));
|
|
35
|
-
if (out.body instanceof ReadableStream) {
|
|
36
|
-
await iterate(out.body, (chunk) => response.write(chunk));
|
|
37
|
-
} else {
|
|
38
|
-
response.write(out.body || "");
|
|
39
|
-
}
|
|
40
|
-
response.end();
|
|
41
|
-
} catch (error) {
|
|
42
|
-
response.writeHead(error.status || 500);
|
|
43
|
-
response.write(error.message || "");
|
|
44
|
-
response.end();
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
.listen(options.port);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const validateOptions = (options, env = {}) => {
|
|
51
|
-
options.port = options.port || env.PORT || 3000;
|
|
52
|
-
options.secret = options.secret || env.SECRET || "unsafe-" + createId();
|
|
53
|
-
options.cors = options.cors || env.CORS || null;
|
|
54
|
-
if (options.cors === true) {
|
|
55
|
-
options.cors = { origin: options.domain || "*" };
|
|
56
|
-
}
|
|
57
|
-
if (typeof options.cors === "string") {
|
|
58
|
-
options.cors = { origin: options.cors };
|
|
59
|
-
}
|
|
60
|
-
if (options.cors && !options.cors.methods) {
|
|
61
|
-
options.cors.methods = "GET,HEAD,POST,PUT,PATCH";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
options.views = options.views ? Bucket(options.views) : null;
|
|
65
|
-
options.public = options.public ? Bucket(options.public) : null;
|
|
66
|
-
options.uploads = options.uploads ? Bucket(options.uploads) : null;
|
|
67
|
-
|
|
68
|
-
options.store = options.store ?? null;
|
|
69
|
-
options.cookies = options.cookies ?? {};
|
|
70
|
-
if (options.store && options.cookies) {
|
|
71
|
-
options.session = { store: options.store.prefix("session:") };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// AUTH
|
|
75
|
-
options.auth = options.auth || env.AUTH || null;
|
|
76
|
-
if (options.auth) {
|
|
77
|
-
if (typeof options.auth !== "object") {
|
|
78
|
-
const [type, provider] = options.auth.split(":");
|
|
79
|
-
options.auth = { type, provider };
|
|
80
|
-
}
|
|
81
|
-
if (typeof options.auth.provider === "string") {
|
|
82
|
-
options.auth.provider === options.auth.provider.split("|");
|
|
83
|
-
}
|
|
84
|
-
if (!options.auth.type) {
|
|
85
|
-
throw new Error("Auth options needs a type");
|
|
86
|
-
}
|
|
87
|
-
if (!options.auth.provider) {
|
|
88
|
-
throw new Error("Auth options needs a provider");
|
|
89
|
-
}
|
|
90
|
-
if (!options.auth.session && options.store) {
|
|
91
|
-
options.auth.session = options.store.prefix("auth:");
|
|
92
|
-
}
|
|
93
|
-
if (!options.auth.store && options.store) {
|
|
94
|
-
options.auth.store = options.store.prefix("user:");
|
|
95
|
-
}
|
|
96
|
-
if (!options.auth.cleanUser) {
|
|
97
|
-
options.auth.cleanUser = (fullUser) => {
|
|
98
|
-
const { password, ...user } = fullUser;
|
|
99
|
-
return user;
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return options;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const extendWithDefaults = (ctx) => {
|
|
108
|
-
// Only want to execute it once; it needs to happen on a per-request
|
|
109
|
-
// basis since we only have full access to the options there
|
|
110
|
-
if (ctx.app.extended) return;
|
|
111
|
-
middle(ctx);
|
|
112
|
-
ctx.app.extended = true;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Export the main server()
|
|
22
|
+
// #region server()
|
|
116
23
|
export default function server(options = {}) {
|
|
117
24
|
// Make it so that the exported one is a prototype of function()
|
|
118
25
|
if (!(this instanceof server)) {
|
|
119
26
|
return new server(options).self();
|
|
120
27
|
}
|
|
121
28
|
|
|
29
|
+
// Keep a copy of the options in the instance
|
|
30
|
+
this.opts = options;
|
|
31
|
+
this.platform = getMachine();
|
|
32
|
+
|
|
33
|
+
// TODO: find a way to remove this hack
|
|
34
|
+
this.extended = false;
|
|
35
|
+
|
|
122
36
|
// Skip "forbidden methods" https://fetch.spec.whatwg.org/#concept-method
|
|
123
37
|
this.handlers = {
|
|
124
38
|
socket: [],
|
|
@@ -131,71 +45,93 @@ export default function server(options = {}) {
|
|
|
131
45
|
options: [],
|
|
132
46
|
};
|
|
133
47
|
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
this.platform = getMachine();
|
|
137
|
-
|
|
138
|
-
// WEBSOCKETS stuff
|
|
139
|
-
const sockets = [];
|
|
48
|
+
this.sockets = [];
|
|
49
|
+
// Note: required by Bun
|
|
140
50
|
this.websocket = {
|
|
141
51
|
message: async (socket, body) => {
|
|
142
52
|
this.handlers.socket
|
|
143
53
|
?.filter((s) => s[0] === "message")
|
|
144
|
-
?.map((s) => s[1]({ socket, sockets, body }));
|
|
54
|
+
?.map((s) => s[1]({ socket, sockets: this.sockets, body }));
|
|
145
55
|
},
|
|
146
|
-
open: (ws) => sockets.push(ws),
|
|
147
|
-
close: (ws) => sockets.splice(sockets.indexOf(ws), 1),
|
|
56
|
+
open: (ws) => this.sockets.push(ws),
|
|
57
|
+
close: (ws) => this.sockets.splice(this.sockets.indexOf(ws), 1),
|
|
148
58
|
};
|
|
149
59
|
|
|
150
|
-
//
|
|
60
|
+
// Initialize it right away for Node.js
|
|
151
61
|
if (this.platform.runtime === "node") {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
createNodeServer(this, options);
|
|
62
|
+
this.node();
|
|
155
63
|
}
|
|
64
|
+
}
|
|
156
65
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} catch (error) {
|
|
166
|
-
return new Response(error.message, { status: error.status || 500 });
|
|
66
|
+
server.prototype.self = function () {
|
|
67
|
+
const cb = this.callback.bind(this);
|
|
68
|
+
const proto = Object.getPrototypeOf(this);
|
|
69
|
+
for (let key in { ...proto, ...this }) {
|
|
70
|
+
if (typeof this[key] === "function") {
|
|
71
|
+
cb[key] = this[key].bind(this);
|
|
72
|
+
} else {
|
|
73
|
+
cb[key] = this[key];
|
|
167
74
|
}
|
|
168
|
-
}
|
|
75
|
+
}
|
|
76
|
+
return cb;
|
|
77
|
+
};
|
|
169
78
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
79
|
+
// #region Runtimes
|
|
80
|
+
// Node.js
|
|
81
|
+
server.prototype.node = async function () {
|
|
82
|
+
const http = await import("http");
|
|
83
|
+
http
|
|
84
|
+
.createServer(async (request, response) => {
|
|
85
|
+
try {
|
|
86
|
+
const options = config(this.opts);
|
|
87
|
+
const ctx = await createNodeContext(request, options, this);
|
|
88
|
+
const out = await handleRequest(this.handlers, ctx);
|
|
89
|
+
|
|
90
|
+
response.writeHead(out.status || 200, parseHeaders(out.headers));
|
|
91
|
+
if (out.body instanceof ReadableStream) {
|
|
92
|
+
await iterate(out.body, (chunk) => response.write(chunk));
|
|
93
|
+
} else {
|
|
94
|
+
response.write(out.body || "");
|
|
95
|
+
}
|
|
96
|
+
response.end();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
response.writeHead(error.status || 500);
|
|
99
|
+
response.write(error.message || "");
|
|
100
|
+
response.end();
|
|
177
101
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
102
|
+
})
|
|
103
|
+
.listen(this.opts.port);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Netlify
|
|
107
|
+
server.prototype.callback = async function (request) {
|
|
108
|
+
try {
|
|
109
|
+
if (typeof Netlify === "undefined") {
|
|
110
|
+
throw new Error("Netlify doesn't exist");
|
|
184
111
|
}
|
|
185
|
-
|
|
186
|
-
|
|
112
|
+
const options = config(this.opts);
|
|
113
|
+
const ctx = await createWinterContext(request, options, this);
|
|
114
|
+
return await handleRequest(this.handlers, ctx);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return new Response(error.message, { status: error.status || 500 });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
187
119
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
120
|
+
// WinterCG, Bun, Cloudflare Workers
|
|
121
|
+
server.prototype.fetch = async function (request, env) {
|
|
122
|
+
if (env?.upgrade(request)) return;
|
|
123
|
+
Object.assign(globalThis.env, env); // Extend env with the passed vars
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const options = config(this.opts);
|
|
127
|
+
const ctx = await createWinterContext(request, options, this);
|
|
128
|
+
return await handleRequest(this.handlers, ctx);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return new Response(error.message, { status: error.status || 500 });
|
|
131
|
+
}
|
|
197
132
|
};
|
|
198
133
|
|
|
134
|
+
// #region HTTP methods
|
|
199
135
|
// INTERNAL
|
|
200
136
|
server.prototype.handle = function (method, path, ...middleware) {
|
|
201
137
|
if (method === "*") {
|
|
@@ -248,6 +184,7 @@ server.prototype.use = function (...middleware) {
|
|
|
248
184
|
return this.handle("*", "*", ...middleware);
|
|
249
185
|
};
|
|
250
186
|
|
|
187
|
+
// Unwind the children routers into the main router
|
|
251
188
|
server.prototype.router = function (basePath, router) {
|
|
252
189
|
basePath = ("/" + basePath + "/").replace(/^\/+/, "/").replace(/\/+$/, "/");
|
|
253
190
|
for (const method in router.handlers) {
|
|
@@ -258,6 +195,7 @@ server.prototype.router = function (basePath, router) {
|
|
|
258
195
|
return this.self();
|
|
259
196
|
};
|
|
260
197
|
|
|
198
|
+
// #region Testing helper
|
|
261
199
|
server.prototype.test = function () {
|
|
262
200
|
let cookie = "";
|
|
263
201
|
const fetch = async (path, options = {}) => {
|
package/src/index.test.js
CHANGED
|
@@ -13,6 +13,21 @@ describe("exports", () => {
|
|
|
13
13
|
|
|
14
14
|
it("export has a fetch", () => {
|
|
15
15
|
expect(typeof server().fetch).toBe("function");
|
|
16
|
+
expect(typeof server().get().fetch).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("export has the basic methods", () => {
|
|
20
|
+
expect(typeof server().get).toBe("function");
|
|
21
|
+
expect(typeof server().post).toBe("function");
|
|
22
|
+
expect(typeof server().use).toBe("function");
|
|
23
|
+
expect(typeof server().router).toBe("function");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("export has the basic nested methods", () => {
|
|
27
|
+
expect(typeof server().get().get).toBe("function");
|
|
28
|
+
expect(typeof server().post().post).toBe("function");
|
|
29
|
+
expect(typeof server().use().use).toBe("function");
|
|
30
|
+
expect(typeof server().get().router).toBe("function");
|
|
16
31
|
});
|
|
17
32
|
|
|
18
33
|
it("nested is also a function", () => {
|
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", () => {
|
package/src/polyfill.js
CHANGED
|
@@ -6,7 +6,13 @@ if (typeof Response === "undefined") {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// Polyfill Netlify's environment variables
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Object.assign(
|
|
9
|
+
globalThis.env = {};
|
|
10
|
+
if (typeof Netlify !== "undefined") {
|
|
11
|
+
Object.assign(env, Netlify.env.toObject());
|
|
12
|
+
}
|
|
13
|
+
if (typeof process !== "undefined") {
|
|
14
|
+
Object.assign(env, process.env);
|
|
15
|
+
}
|
|
16
|
+
if (typeof import.meta.env !== "undefined") {
|
|
17
|
+
Object.assign(env, import.meta.env);
|
|
12
18
|
}
|
|
File without changes
|