@server/next 0.20.31 → 0.20.32

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,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.32",
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",
package/readme.md CHANGED
@@ -119,3 +119,46 @@ Context docs here
119
119
  ## Reply
120
120
 
121
121
  Reply docs here
122
+
123
+ ## Runtimes
124
+
125
+ 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:
126
+
127
+ - Node.js of course, including everywhere that Node is supported (VPS, Heroku, Render, etc).
128
+ - Bun, and it will be even faster!
129
+ - Cloudflare Worker
130
+ - Netlify Function + Edge function
131
+
132
+ ## FAQ
133
+
134
+ #### How is it different from Hono?
135
+
136
+ 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:
137
+
138
+ ```js
139
+ // Server.js code for Node.js, Bun and Netlify
140
+ import server from "@server/next";
141
+ export default server().get("/", () => "Hello server!");
142
+ ```
143
+
144
+ ```js
145
+ // Hono code for Node.js
146
+ import { serve } from '@hono/node-server'
147
+ import { Hono } from 'hono'
148
+ const app = new Hono()
149
+ app.get('/', (c) => c.text('Hello Node.js!'))
150
+ serve(app)
151
+
152
+ // Hono code for Bun
153
+ import { Hono } from 'hono'
154
+ const app = new Hono()
155
+ app.get('/', (c) => c.text('Hello Bun!'))
156
+ export default app
157
+
158
+ // Hono code for Netlify
159
+ import { Hono } from 'jsr:@hono/hono'
160
+ import { handle } from 'jsr:@hono/hono/netlify'
161
+ const app = new Hono()
162
+ app.get('/', (c) => c.text('Hello Hono!'))
163
+ export default handle(app)
164
+ ```
@@ -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,9 +1,20 @@
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
+ // Only want to execute it once; it needs to happen on a per-request
9
+ // basis since we only have full access to the options there
10
+ if (ctx.app.extended) return;
11
+ middle(ctx);
12
+ ctx.app.extended = true;
13
+ };
14
+
6
15
  export default async function handleRequest(handlers, ctx) {
16
+ extendWithDefaults(ctx);
17
+
7
18
  for (let [method, matcher, ...cbs] of handlers[ctx.method]) {
8
19
  const match = pathPattern(matcher, ctx.url.pathname || "/");
9
20
  // Skip this whole middleware if there was no match
@@ -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", () => {
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