@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 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,8 +1,8 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.20.31",
4
- "description": "An experimental reimplementation of server.js focused on the DX",
5
- "homepage": "https://node-server.com/",
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
- "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
@@ -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 "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
  };
@@ -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
  }
@@ -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
- createId,
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
- const createNodeServer = async (app, options) => {
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.extended = false;
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
- // Starting stuff
60
+ // Initialize it right away for Node.js
151
61
  if (this.platform.runtime === "node") {
152
- options = validateOptions(options, process.env);
153
-
154
- createNodeServer(this, options);
62
+ this.node();
155
63
  }
64
+ }
156
65
 
157
- this.fetch = async (request, env, fetchCtx) => {
158
- if (env?.upgrade(request)) return;
159
-
160
- try {
161
- options = validateOptions(options, env);
162
- const ctx = await createWinterContext(request, options, this);
163
- extendWithDefaults(ctx);
164
- return await handleRequest(this.handlers, ctx);
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
- this.netlify = async (request) => {
171
- try {
172
- if (typeof Netlify === "undefined") {
173
- throw new Error("Netlify doesn't exist");
174
- }
175
- if (typeof import.meta === "undefined") {
176
- throw new Error("import.meta.env doesn't exist");
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
- options = validateOptions(options, import.meta.env);
179
- const ctx = await createWinterContext(request, options, this);
180
- extendWithDefaults(ctx);
181
- return await handleRequest(this.handlers, ctx);
182
- } catch (error) {
183
- return new Response(error.message, { status: error.status || 500 });
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
- server.prototype.self = function () {
189
- const cb = this.netlify;
190
- const keys = Object.keys(Object.getPrototypeOf(this)).concat(
191
- Object.keys(this)
192
- );
193
- keys.forEach((key) => {
194
- cb[key] = this[key];
195
- });
196
- return cb;
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", () => {
@@ -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", () => {
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
- if (typeof Netlify !== "undefined" && typeof import.meta !== "undefined") {
10
- if (!import.meta.env) import.meta.env = {};
11
- Object.assign(import.meta.env, Netlify.env.toObject());
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