@server/next 0.20.30 → 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.30",
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,67 +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);
62
+ this.node();
63
+ }
64
+ }
153
65
 
154
- createNodeServer(this, options);
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];
74
+ }
155
75
  }
76
+ return cb;
77
+ };
156
78
 
157
- this.fetch = async (request, env, fetchCtx) => {
158
- if (env?.upgrade(request)) return;
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);
159
89
 
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 });
167
- }
168
- };
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();
101
+ }
102
+ })
103
+ .listen(this.opts.port);
104
+ };
169
105
 
170
- this.netlify = async (request) => {
171
- if (typeof Netlify === "undefined") throw new Error("Unknown");
172
- console.log("Experimental Netlify");
173
- try {
174
- options = validateOptions(options, Netlify.env.toObject());
175
- const ctx = await createWinterContext(request, options, this);
176
- extendWithDefaults(ctx);
177
- return await handleRequest(this.handlers, ctx);
178
- } catch (error) {
179
- return new Response(error.message, { status: error.status || 500 });
106
+ // Netlify
107
+ server.prototype.callback = async function (request) {
108
+ try {
109
+ if (typeof Netlify === "undefined") {
110
+ throw new Error("Netlify doesn't exist");
180
111
  }
181
- };
182
- }
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
+ };
183
119
 
184
- server.prototype.self = function () {
185
- const cb = this.netlify;
186
- const keys = Object.keys(Object.getPrototypeOf(this)).concat(
187
- Object.keys(this)
188
- );
189
- keys.forEach((key) => {
190
- cb[key] = this[key];
191
- });
192
- 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
+ }
193
132
  };
194
133
 
134
+ // #region HTTP methods
195
135
  // INTERNAL
196
136
  server.prototype.handle = function (method, path, ...middleware) {
197
137
  if (method === "*") {
@@ -244,6 +184,7 @@ server.prototype.use = function (...middleware) {
244
184
  return this.handle("*", "*", ...middleware);
245
185
  };
246
186
 
187
+ // Unwind the children routers into the main router
247
188
  server.prototype.router = function (basePath, router) {
248
189
  basePath = ("/" + basePath + "/").replace(/^\/+/, "/").replace(/\/+$/, "/");
249
190
  for (const method in router.handlers) {
@@ -254,6 +195,7 @@ server.prototype.router = function (basePath, router) {
254
195
  return this.self();
255
196
  };
256
197
 
198
+ // #region Testing helper
257
199
  server.prototype.test = function () {
258
200
  let cookie = "";
259
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