@jnode/server 2.0.2 → 2.2.0

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/README.md CHANGED
@@ -83,6 +83,7 @@ Also, we provide some powerful built-in routers and handlers so you can start bu
83
83
  - `options` [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
84
84
  - `maxRoutingSteps` [\<number\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type) The max steps for routing; when exceeded but still getting another router, it'll throw the client a **508** error. **Default:** `50`.
85
85
  - `enableHTTP2` [\<boolean\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type) Enable HTTP/2 support (with `node:http2` [Compatibility API](https://nodejs.org/docs/latest/api/http2.html#compatibility-api)). **Default:** `false`.
86
+ - `codeHandlers` [\<Object\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) Global status code handlers. Keys are HTTP status codes, values are [handlers-extended](#handler-extended).
86
87
  - Options in [`http.createServer()`](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener), [`https.createServer()`](https://nodejs.org/docs/latest/api/https.html#httpscreateserveroptions-requestlistener), [`http2.createServer()`](https://nodejs.org/docs/latest/api/http2.html#http2createserveroptions-onrequesthandler), or [`http2.createSecureServer()`](https://nodejs.org/docs/latest/api/http2.html#http2createsecureserveroptions-onrequesthandler).
87
88
  - Returns: [\<server.Server\>](#class-serverserver)
88
89
 
@@ -264,10 +265,13 @@ We provide two methods to use them:
264
265
  - `<METHOD>/<path_segment>[/<path_segment>...]` [router](#class-serverrouter) | [handler-extended](#handler-extended) Used when the method matches; equals to `'/<path_segment>': r.Method({ '<METHOD>': <value> })`. E.G., `'GET/meow': 'Meow!'` (only works for HTTP method `'GET'`).
265
266
  - `@<METHOD>/<path_segment>[/<path_segment>...]` [router](#class-serverrouter) | [handler-extended](#handler-extended) Used when both the path resolver ends and the method matches; equals to `'/<path_segment>': r.Path(r.Method({ '<METHOD>': <value> }))`. E.G., `'@GET/meow': 'Meow!'` (only works for path `'/meow'` but not `'/meow/something'` and request method is `'GET'`).
266
267
  - `*` [router](#class-serverrouter) | [handler-extended](#handler-extended) Any path segment. E.G. `'*': h.Text('Meow? Nothing here!', { statusCode: 404 })`.
268
+ - `/%:<path_parameter_name>` [router](#class-serverrouter) | [handler-extended](#handler-extended) Match any segment (if exists) and save the segment to `ctx.params` by `<path_parameter_name>`. Do the similar thing as [`PathArgRouter`](#router-pathargrouterparamname-next).
267
269
 
268
270
  `PathRouter` is probably the most important router; almost every server needs it!
269
271
 
270
- By the way, we **DO NOT** support `'/*'` as a universal matching character, don't try this.
272
+ Please note that when defining only `/%:arg/b` and `/a/c`, requesting `/a/b` **WILL NOT** return the value of `/%:arg/b`. Instead, it will return `404`. This limitation is in place to improve performance, and we believe in designing a great API. If you still need this functionality, you can build your own router.
273
+
274
+ By the way, if you’re looking for a universal matching character, use `'/%:'` instead of `'/*'` (this will match to `'*'` in a literal sense).
271
275
 
272
276
  #### How it works?
273
277
 
@@ -283,7 +287,8 @@ map = {
283
287
  '@/a/b': 'C!',
284
288
  'GET/a': 'D!',
285
289
  '@GET/a/b': 'E!',
286
- '*': 'F!'
290
+ '@GET/%:arg/c': 'F!',
291
+ '*': 'G!'
287
292
  };
288
293
 
289
294
  // we will parse into
@@ -305,11 +310,19 @@ parsed = {
305
310
  }
306
311
  }
307
312
  },
308
- '*': 'F!'
313
+ ':': {
314
+ '/c': {
315
+ '@': {
316
+ 'GET': 'F!',
317
+ '::GET': [ 'arg' ]
318
+ }
319
+ }
320
+ },
321
+ '*': 'G!'
309
322
  };
310
323
  ```
311
324
 
312
- This format enables fast and flexible path routing while keeping it simple for developers.
325
+ This format allows for fast and flexible path routing while maintaining simplicity for developers. However, as mentioned earlier, some specialized path matching will not be supported.
313
326
 
314
327
  ### Router: `HostRouter(end, map)`
315
328
 
@@ -318,6 +331,7 @@ This format enables fast and flexible path routing while keeping it simple for d
318
331
  - `.<host_segment>[.<host_segment>...]` [router](#class-serverrouter) | [handler-extended](#handler-extended) A simple host segment routing (reversed). E.G., `.example.com` will match `example.com`, `.localhost` will match `localhost`.
319
332
  - `@.<host_segment>[.<host_segment>...]` [router](#class-serverrouter) | [handler-extended](#handler-extended) Used when the host resolver ends here. E.G., `@.example.com` (only works for exactly `example.com` but not `sub.example.com`).
320
333
  - `*` [router](#class-serverrouter) | [handler-extended](#handler-extended) Any host segment.
334
+ - `.%:<host_parameter_name>` [router](#class-serverrouter) | [handler-extended](#handler-extended) Match any segment (if exists) and save the segment to `ctx.params` by `<host_parameter_name>`.
321
335
 
322
336
  ### Router: `MethodRouter(methodMap)`
323
337
 
@@ -326,9 +340,10 @@ This format enables fast and flexible path routing while keeping it simple for d
326
340
  - `*` [router](#class-serverrouter) | [handler-extended](#handler-extended) Any method, used as fallback.
327
341
  - Returns: [\<MethodRouter\>](#router-methodroutermethodmap) Routes based on HTTP method, returns 405 if no method matches.
328
342
 
329
- ### Router: `FunctionRouter(fn)`
343
+ ### Router: `FunctionRouter(fn, ext)`
330
344
 
331
- - `fn` [\<Function\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) A function with signature `(env, ctx) => router | handler-extended`.
345
+ - `fn` [\<Function\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) A function with signature `(env, ctx, ext) => router | handler-extended`.
346
+ - `ext` [\<any\>] Passed to `func`.
332
347
 
333
348
  A simple router that allows you to implement custom routing logic.
334
349
 
@@ -409,8 +424,9 @@ Sends a JavaScript object serialized as JSON with `Content-Type: application/jso
409
424
 
410
425
  Redirects the request to a specified location. Supports both absolute URLs and dynamic redirects based on remaining path segments.
411
426
 
412
- ### Handler: `FunctionHandler(func)`
427
+ ### Handler: `FunctionHandler(func, ext)`
413
428
 
414
- - `func` [\<Function\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) A function with signature `(ctx, env) => void | Promise<void>`.
429
+ - `func` [\<Function\>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) A function with signature `(ctx, env, ext) => void | Promise<void>`.
430
+ - `ext` [\<any\>] Passed to `func`.
415
431
 
416
432
  Allows you to implement custom request handling logic directly within a function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jnode/server",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "Simple web server package for Node.js.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/handlers.js CHANGED
@@ -18,28 +18,38 @@ class DataHandler {
18
18
  constructor(data, options = {}) {
19
19
  this.data = data;
20
20
  this.options = options;
21
- }
22
21
 
23
- async handle(ctx, env) {
22
+ // prebuild headers
23
+ this._statusCode = this.options.statusCode ?? 200;
24
24
  if (typeof this.data === 'string') { // string
25
- ctx.res.writeHead(this.options.statusCode ?? 200, {
25
+ this.data = Buffer.from(this.data, 'utf8');
26
+ this._headers = {
26
27
  'Content-Type': 'text/plain; charset=utf-8',
27
- 'Content-Length': Buffer.byteLength(this.data, 'utf8'),
28
+ 'Content-Length': this.data.length,
28
29
  ...this.options.headers
29
- });
30
- ctx.res.end(this.data, 'utf8');
30
+ };
31
31
  } else if (this.data instanceof Uint8Array) { // buffer
32
- ctx.res.writeHead(this.options.statusCode ?? 200, {
32
+ this._headers = {
33
33
  'Content-Type': 'application/octet-stream',
34
34
  'Content-Length': this.data.length,
35
35
  ...this.options.headers
36
- });
37
- ctx.res.end(this.data);
38
- } else if (stream.isReadable(this.data)) { // stream
39
- ctx.res.writeHead(this.options.statusCode ?? 200, {
36
+ };
37
+ } else if (stream.isReadable(this.data)) {
38
+ this._headers = {
40
39
  'Content-Type': 'application/octet-stream',
41
40
  ...this.options.headers
42
- });
41
+ };
42
+ } else {
43
+ throw new Error('Unsupported data type.');
44
+ }
45
+ }
46
+
47
+ async handle(ctx, env) {
48
+ if (this.data instanceof Uint8Array) { // buffer
49
+ ctx.res.writeHead(this._statusCode, this._headers);
50
+ ctx.res.end(this.data);
51
+ } else if (stream.isReadable(this.data)) { // stream
52
+ ctx.res.writeHead(this._statusCode, this._headers);
43
53
 
44
54
  try {
45
55
  await stream.promises.pipeline(this.data, ctx.res);
@@ -184,19 +194,21 @@ class FolderHandler {
184
194
  // JSON handler: JSON object
185
195
  class JSONHandler {
186
196
  constructor(obj, options = {}) {
187
- this.obj = obj;
197
+ this.data = Buffer.from(JSON.stringify(obj), 'utf8');
188
198
  this.options = options;
189
- }
190
199
 
191
- handle(ctx, env) {
192
- const data = JSON.stringify(this.obj);
193
-
194
- ctx.res.writeHead(this.options.statusCode ?? 200, {
200
+ // prebuild headers
201
+ this._statusCode = this.options.statusCode ?? 200;
202
+ this._headers = {
195
203
  'Content-Type': 'application/json; charset=utf-8',
196
204
  'Content-Length': Buffer.byteLength(data, 'utf8'),
197
205
  ...this.options.headers
198
- });
199
- ctx.res.end(data, 'utf8');
206
+ };
207
+ }
208
+
209
+ handle(ctx, env) {
210
+ ctx.res.writeHead(this.statusCode, this._headers);
211
+ ctx.res.end(this.data);
200
212
  }
201
213
  }
202
214
 
@@ -205,29 +217,34 @@ class RedirectHandler {
205
217
  constructor(location, options = {}) {
206
218
  this.location = location;
207
219
  this.options = options;
208
- }
209
220
 
210
- handle(ctx, env) {
211
- ctx.res.writeHead(this.options.statusCode ?? 307, {
221
+ // prebuild headers
222
+ this._statusCode = this.options.statusCode ?? 307;
223
+ this._headers = {
212
224
  'Location': this.options.base ?
213
225
  this.options.base +
214
226
  (this.options.base.endsWith('/') ? '' : '/') +
215
227
  env.path.slice(env.pathPointer).map(encodeURIComponent).join('/') :
216
228
  this.location,
217
229
  ...this.options.headers
218
- });
230
+ };
231
+ }
232
+
233
+ handle(ctx, env) {
234
+ ctx.res.writeHead(this._statusCode, this._headers);
219
235
  ctx.res.end();
220
236
  }
221
237
  }
222
238
 
223
239
  // function handler: custom function
224
240
  class FunctionHandler {
225
- constructor(func) {
241
+ constructor(func, ext) {
226
242
  this.func = func;
243
+ this.ext = ext;
227
244
  }
228
245
 
229
246
  handle(ctx, env) {
230
- return this.func(ctx, env);
247
+ return this.func(ctx, env, this.ext);
231
248
  }
232
249
  }
233
250
 
@@ -235,12 +252,12 @@ class FunctionHandler {
235
252
  module.exports = {
236
253
  DataHandler, TextHandler: DataHandler, FileHandler, FolderHandler, JSONHandler, RedirectHandler, FunctionHandler,
237
254
  handlerConstructors: {
238
- Data: (...args) => new DataHandler(...args),
239
- Text: (...args) => new DataHandler(...args),
240
- File: (...args) => new FileHandler(...args),
241
- Folder: (...args) => new FolderHandler(...args),
242
- JSON: (...args) => new JSONHandler(...args),
243
- Redirect: (...args) => new RedirectHandler(...args),
244
- Function: (...args) => new FunctionHandler(...args)
255
+ Data: (data, options) => new DataHandler(data, options),
256
+ Text: (data, options) => new DataHandler(data, options),
257
+ File: (file, options) => new FileHandler(file, options),
258
+ Folder: (folder, options) => new FolderHandler(folder, options),
259
+ JSON: (obj, options) => new JSONHandler(obj, options),
260
+ Redirect: (location, options) => new RedirectHandler(location, options),
261
+ Function: (func, ext) => new FunctionHandler(func, ext)
245
262
  }
246
263
  };
package/src/routers.js CHANGED
@@ -29,20 +29,30 @@ class PathRouter {
29
29
  // key format: '[@ (path end check)][METHOD (method check)]/path/segments'
30
30
  // example: '@ GET /cat/names', 'POST /cats', '/api'
31
31
  const routeEnd = key.startsWith('@');
32
- const routeMethod = key.substring(routeEnd ? 1 : 0, firstSlashIndex).trim().toUpperCase();
33
- const routePath = key.substring(firstSlashIndex).split('/').slice(1).map(decodeURIComponent);
32
+ const routeMethod = key.substring(routeEnd ? 1 : 0, firstSlashIndex).trim();
33
+ const routePath = key.substring(firstSlashIndex).split('/').slice(1);
34
34
 
35
35
  // expand map
36
36
  let current = this.map;
37
- for (const segment of routePath) {
38
- if (!current['/' + segment]) current['/' + segment] = {};
39
- current = current['/' + segment];
37
+ let args = [];
38
+ for (let segment of routePath) {
39
+ if (segment.startsWith('%:')) {
40
+ args.push(segment.substring(2));
41
+ if (!current[':']) current[':'] = {};
42
+ current = current[':'];
43
+ continue;
44
+ }
45
+
46
+ segment = '/' + decodeURIComponent(segment);
47
+ if (!current[segment]) current[segment] = {};
48
+ current = current[segment];
40
49
  }
41
50
 
42
51
  // '*' for non-end check, '@' for end check
43
52
  // '*' for any method, 'METHOD' for specific method
44
53
  if (!current[routeEnd ? '@' : '*']) current[routeEnd ? '@' : '*'] = {};
45
54
  current[routeEnd ? '@' : '*'][routeMethod || '*'] = value;
55
+ if (args.length > 0) current[routeEnd ? '@' : '*']['::' + (routeMethod || '*')] = args;
46
56
  }
47
57
  }
48
58
 
@@ -52,14 +62,21 @@ class PathRouter {
52
62
  let result = this.map['*'];
53
63
  let resultPointer = env.pathPointer;
54
64
  let current = this.map;
65
+ let currentArgs = [];
66
+ let resultArgNames;
55
67
  while (env.pathPointer < env.path.length) {
56
- const segment = '/' + env.path[env.pathPointer];
57
- if (!current[segment]) break;
68
+ let segment = '/' + env.path[env.pathPointer];
69
+ if (!current[segment] && !current[':']) break;
70
+ if (!current[segment]) {
71
+ segment = ':';
72
+ currentArgs.push(env.path[env.pathPointer]);
73
+ }
58
74
 
59
75
  // prepare fallback
60
76
  if (current[segment]['*']?.['*'] || current[segment]['*']?.[ctx.method]) {
61
77
  result = current[segment]['*'][ctx.method] ?? current[segment]['*']['*'];
62
78
  resultPointer = env.pathPointer + 1;
79
+ resultArgNames = current[segment]['*']['::' + ctx.method] ?? current[segment]['*']['::*'];
63
80
  }
64
81
 
65
82
  current = current[segment];
@@ -69,10 +86,17 @@ class PathRouter {
69
86
  if (env.pathPointer >= env.path.length && (current['@']?.['*'] || current['@']?.[ctx.method])) {
70
87
  result = current['@'][ctx.method] ?? current['@']['*'];
71
88
  resultPointer = env.pathPointer;
89
+ resultArgNames = current['@']['::' + ctx.method] ?? current['@']['::*'];
72
90
  }
73
91
  }
74
92
 
75
93
  env.pathPointer = resultPointer;
94
+ if (resultArgNames) {
95
+ const len = resultArgNames.length;
96
+ for (let i = 0; i < len; i++) {
97
+ ctx.params[resultArgNames[i]] = currentArgs[i];
98
+ }
99
+ }
76
100
  return result;
77
101
  }
78
102
  }
@@ -103,13 +127,22 @@ class HostRouter {
103
127
 
104
128
  // expand map
105
129
  let current = this.map;
130
+ let args = [];
106
131
  for (const segment of routeDomain) {
132
+ if (segment.startsWith('%:')) {
133
+ args.push(segment.substring(2));
134
+ if (!current[':']) current[':'] = {};
135
+ current = current[':'];
136
+ continue;
137
+ }
138
+
107
139
  if (!current['.' + segment]) current['.' + segment] = {};
108
140
  current = current['.' + segment];
109
141
  }
110
142
 
111
143
  // '*' for non-end check, '@' for end check
112
144
  current[routeEnd ? '@' : '*'] = value;
145
+ if (args.length > 0) current['::'] = args;
113
146
  }
114
147
  }
115
148
 
@@ -119,14 +152,21 @@ class HostRouter {
119
152
  let result = this.map['*'];
120
153
  let resultPointer = env.hostPointer;
121
154
  let current = this.map;
155
+ let currentArgs = [];
156
+ let resultArgNames;
122
157
  while (env.hostPointer < env.host.length) {
123
- const segment = '.' + env.host[env.hostPointer];
124
- if (!current[segment]) break;
158
+ let segment = '.' + env.host[env.hostPointer];
159
+ if (!current[segment] && !current[':']) break;
160
+ if (!current[segment]) {
161
+ segment = ':';
162
+ currentArgs.push(env.host[env.hostPointer]);
163
+ }
125
164
 
126
165
  // prepare fallback
127
166
  if (current[segment]['*']) {
128
167
  result = current[segment]['*'];
129
168
  resultPointer = env.hostPointer + 1;
169
+ resultArgNames = current[segment]['::'];
130
170
  }
131
171
 
132
172
  current = current[segment];
@@ -140,6 +180,12 @@ class HostRouter {
140
180
  }
141
181
 
142
182
  env.hostPointer = resultPointer;
183
+ if (resultArgNames) {
184
+ const len = resultArgNames.length;
185
+ for (let i = 0; i < len; i++) {
186
+ ctx.params[resultArgNames[i]] = currentArgs[i];
187
+ }
188
+ }
143
189
  return result;
144
190
  }
145
191
  }
@@ -157,12 +203,13 @@ class MethodRouter {
157
203
 
158
204
  // function router: a simple router that allows you to make custom routing logic
159
205
  class FunctionRouter {
160
- constructor(fn) {
206
+ constructor(fn, ext) {
161
207
  this.fn = fn;
208
+ this.ext = ext;
162
209
  }
163
210
 
164
211
  route(env, ctx) {
165
- return this.fn(env, ctx);
212
+ return this.fn(env, ctx, this.ext);
166
213
  }
167
214
  }
168
215
 
@@ -213,12 +260,11 @@ class SetCodeRouter {
213
260
  module.exports = {
214
261
  PathRouter, HostRouter, MethodRouter, FunctionRouter, PathArgRouter, HostArgRouter, SetCodeRouter,
215
262
  routerConstructors: {
216
- Path: (...args) => new PathRouter(...args),
217
- Host: (...args) => new HostRouter(...args),
218
- Method: (...args) => new MethodRouter(...args),
219
- Function: (...args) => new FunctionRouter(...args),
220
- PathArg: (...args) => new PathArgRouter(...args),
221
- HostArg: (...args) => new HostArgRouter(...args),
222
- SetCode: (...args) => new SetCodeRouter(...args)
263
+ Path: (end, map) => new PathRouter(end, map),
264
+ Host: (end, map) => new HostRouter(end, map),
265
+ Method: (methodMap) => new MethodRouter(methodMap),
266
+ Function: (fn, ext) => new FunctionRouter(fn, ext),
267
+ PathArg: (name, next) => new PathArgRouter(name, next),
268
+ SetCode: (handlers, next) => new SetCodeRouter(handlers, next)
223
269
  }
224
270
  };
package/src/server.js CHANGED
@@ -14,6 +14,7 @@ const http2 = require('http2');
14
14
  const path = require('path');
15
15
  const stream = require('stream');
16
16
  const EventEmitter = require('events');
17
+ const constants = require('constants');
17
18
 
18
19
  // server class
19
20
  class Server extends EventEmitter {
@@ -106,11 +107,12 @@ class Server extends EventEmitter {
106
107
  } else if (typeof handler === 'function') { // function
107
108
  await handler(ctx, env);
108
109
  } else if (typeof handler === 'string') { // string
110
+ const data = Buffer.from(handler, 'utf8');
109
111
  ctx.res.writeHead(200, {
110
112
  'Content-Type': 'text/plain; charset=utf-8',
111
- 'Content-Length': Buffer.byteLength(handler, 'utf8')
113
+ 'Content-Length': data.length
112
114
  });
113
- ctx.res.end(handler, 'utf8');
115
+ ctx.res.end(data);
114
116
  } else if (handler instanceof Uint8Array) { // buffer
115
117
  ctx.res.writeHead(200, {
116
118
  'Content-Type': 'application/octet-stream',
@@ -130,11 +132,12 @@ class Server extends EventEmitter {
130
132
  } else if (typeof handler === 'function') { // function
131
133
  await handler(ctx, env);
132
134
  } else if (typeof handler === 'string') { // string
135
+ const data = Buffer.from(handler, 'utf8');
133
136
  ctx.res.writeHead(code, {
134
137
  'Content-Type': 'text/plain; charset=utf-8',
135
- 'Content-Length': Buffer.byteLength(handler, 'utf8')
138
+ 'Content-Length': data.length
136
139
  });
137
- ctx.res.end(handler, 'utf8');
140
+ ctx.res.end(data);
138
141
  } else if (handler instanceof Uint8Array) { // buffer
139
142
  ctx.res.writeHead(code, {
140
143
  'Content-Type': 'application/octet-stream',
@@ -164,11 +167,12 @@ class Server extends EventEmitter {
164
167
  } else if (typeof handler === 'function') { // function
165
168
  await handler(ctx, env);
166
169
  } else if (typeof handler === 'string') { // string
170
+ const data = Buffer.from(handler, 'utf8');
167
171
  ctx.res.writeHead(code, {
168
172
  'Content-Type': 'text/plain; charset=utf-8',
169
- 'Content-Length': Buffer.byteLength(handler, 'utf8')
173
+ 'Content-Length': data.length
170
174
  });
171
- ctx.res.end(handler, 'utf8');
175
+ ctx.res.end(data);
172
176
  } else if (handler instanceof Uint8Array) { // buffer
173
177
  ctx.res.writeHead(code, {
174
178
  'Content-Type': 'application/octet-stream',