@mojojs/core 1.26.18 → 2.0.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.
Files changed (156) hide show
  1. package/README.md +1 -1
  2. package/lib/app.d.ts +31 -16
  3. package/lib/app.js +181 -126
  4. package/lib/body/params.js +0 -1
  5. package/lib/body.d.ts +13 -5
  6. package/lib/body.js +47 -17
  7. package/lib/cache.d.ts +1 -2
  8. package/lib/cache.js +12 -21
  9. package/lib/cgi.d.ts +3 -3
  10. package/lib/cgi.js +16 -8
  11. package/lib/cli/cgi.d.ts +1 -1
  12. package/lib/cli/cgi.js +1 -2
  13. package/lib/cli/create-full-app.d.ts +1 -1
  14. package/lib/cli/create-full-app.js +27 -27
  15. package/lib/cli/create-lite-app.d.ts +1 -1
  16. package/lib/cli/create-lite-app.js +2 -3
  17. package/lib/cli/create-plugin.d.ts +1 -1
  18. package/lib/cli/create-plugin.js +25 -23
  19. package/lib/cli/eval.d.ts +1 -1
  20. package/lib/cli/eval.js +12 -5
  21. package/lib/cli/get.d.ts +1 -1
  22. package/lib/cli/get.js +30 -26
  23. package/lib/cli/repl.d.ts +1 -1
  24. package/lib/cli/repl.js +0 -1
  25. package/lib/cli/routes.d.ts +1 -1
  26. package/lib/cli/routes.js +10 -7
  27. package/lib/cli/server.d.ts +1 -1
  28. package/lib/cli/server.js +41 -24
  29. package/lib/cli/version.js +1 -2
  30. package/lib/cli.d.ts +1 -1
  31. package/lib/cli.js +25 -19
  32. package/lib/context.d.ts +33 -10
  33. package/lib/context.js +137 -32
  34. package/lib/core.d.ts +14 -13
  35. package/lib/core.js +12 -12
  36. package/lib/headers.js +5 -4
  37. package/lib/hooks.d.ts +1 -1
  38. package/lib/hooks.js +1 -5
  39. package/lib/logger.d.ts +2 -2
  40. package/lib/logger.js +28 -16
  41. package/lib/mime.js +10 -13
  42. package/lib/plugins/default-conditions.d.ts +1 -1
  43. package/lib/plugins/default-conditions.js +0 -1
  44. package/lib/plugins/default-helpers.d.ts +1 -1
  45. package/lib/plugins/default-helpers.js +28 -11
  46. package/lib/plugins/json-config.d.ts +1 -1
  47. package/lib/plugins/json-config.js +0 -1
  48. package/lib/plugins/mount.d.ts +1 -1
  49. package/lib/plugins/mount.js +0 -1
  50. package/lib/plugins/tmpl-engine.d.ts +1 -1
  51. package/lib/plugins/tmpl-engine.js +2 -5
  52. package/lib/plugins/yaml-config.d.ts +1 -1
  53. package/lib/plugins/yaml-config.js +1 -2
  54. package/lib/renderer.d.ts +1 -1
  55. package/lib/renderer.js +27 -30
  56. package/lib/router/pattern.d.ts +1 -1
  57. package/lib/router/pattern.js +25 -14
  58. package/lib/router/plan.d.ts +1 -1
  59. package/lib/router/plan.js +12 -15
  60. package/lib/router/route.d.ts +10 -3
  61. package/lib/router/route.js +44 -38
  62. package/lib/router.d.ts +4 -4
  63. package/lib/router.js +25 -26
  64. package/lib/server/cookie.d.ts +1 -1
  65. package/lib/server/cookie.js +3 -2
  66. package/lib/server/request.d.ts +3 -3
  67. package/lib/server/request.js +44 -17
  68. package/lib/server/response.d.ts +3 -3
  69. package/lib/server/response.js +20 -20
  70. package/lib/server.d.ts +7 -2
  71. package/lib/server.js +70 -20
  72. package/lib/session.d.ts +8 -3
  73. package/lib/session.js +47 -29
  74. package/lib/sse.d.ts +10 -0
  75. package/lib/sse.js +62 -0
  76. package/lib/static.d.ts +1 -1
  77. package/lib/static.js +27 -22
  78. package/lib/types.d.ts +13 -5
  79. package/lib/types.js +0 -1
  80. package/lib/user-agent/cookie-jar.d.ts +2 -2
  81. package/lib/user-agent/cookie-jar.js +6 -9
  82. package/lib/user-agent/mock.d.ts +4 -4
  83. package/lib/user-agent/mock.js +6 -7
  84. package/lib/user-agent/response.d.ts +12 -1
  85. package/lib/user-agent/response.js +36 -2
  86. package/lib/user-agent/test.d.ts +75 -5
  87. package/lib/user-agent/test.js +147 -13
  88. package/lib/user-agent/transport/http.d.ts +2 -2
  89. package/lib/user-agent/transport/http.js +7 -10
  90. package/lib/user-agent/transport/https.d.ts +2 -2
  91. package/lib/user-agent/transport/https.js +2 -6
  92. package/lib/user-agent/transport/ws.d.ts +2 -2
  93. package/lib/user-agent/transport/ws.js +2 -3
  94. package/lib/user-agent.d.ts +21 -18
  95. package/lib/user-agent.js +70 -49
  96. package/lib/util.d.ts +13 -0
  97. package/lib/util.js +35 -11
  98. package/lib/validator/result.d.ts +1 -1
  99. package/lib/validator/result.js +8 -1
  100. package/lib/validator.d.ts +2 -2
  101. package/lib/validator.js +3 -6
  102. package/lib/websocket.d.ts +2 -2
  103. package/lib/websocket.js +9 -1
  104. package/package.json +22 -29
  105. package/lib/app.js.map +0 -1
  106. package/lib/body/params.js.map +0 -1
  107. package/lib/body.js.map +0 -1
  108. package/lib/cache.js.map +0 -1
  109. package/lib/cgi.js.map +0 -1
  110. package/lib/cli/cgi.js.map +0 -1
  111. package/lib/cli/create-full-app.js.map +0 -1
  112. package/lib/cli/create-lite-app.js.map +0 -1
  113. package/lib/cli/create-plugin.js.map +0 -1
  114. package/lib/cli/eval.js.map +0 -1
  115. package/lib/cli/get.js.map +0 -1
  116. package/lib/cli/repl.js.map +0 -1
  117. package/lib/cli/routes.js.map +0 -1
  118. package/lib/cli/server.js.map +0 -1
  119. package/lib/cli/version.js.map +0 -1
  120. package/lib/cli.js.map +0 -1
  121. package/lib/context.js.map +0 -1
  122. package/lib/core.js.map +0 -1
  123. package/lib/headers.js.map +0 -1
  124. package/lib/hooks.js.map +0 -1
  125. package/lib/logger.js.map +0 -1
  126. package/lib/mime.js.map +0 -1
  127. package/lib/plugins/default-conditions.js.map +0 -1
  128. package/lib/plugins/default-helpers.js.map +0 -1
  129. package/lib/plugins/json-config.js.map +0 -1
  130. package/lib/plugins/mount.js.map +0 -1
  131. package/lib/plugins/tmpl-engine.js.map +0 -1
  132. package/lib/plugins/yaml-config.js.map +0 -1
  133. package/lib/renderer.js.map +0 -1
  134. package/lib/router/pattern.js.map +0 -1
  135. package/lib/router/plan.js.map +0 -1
  136. package/lib/router/route.js.map +0 -1
  137. package/lib/router.js.map +0 -1
  138. package/lib/server/cookie.js.map +0 -1
  139. package/lib/server/request.js.map +0 -1
  140. package/lib/server/response.js.map +0 -1
  141. package/lib/server.js.map +0 -1
  142. package/lib/session.js.map +0 -1
  143. package/lib/static.js.map +0 -1
  144. package/lib/types.js.map +0 -1
  145. package/lib/user-agent/cookie-jar.js.map +0 -1
  146. package/lib/user-agent/mock.js.map +0 -1
  147. package/lib/user-agent/response.js.map +0 -1
  148. package/lib/user-agent/test.js.map +0 -1
  149. package/lib/user-agent/transport/http.js.map +0 -1
  150. package/lib/user-agent/transport/https.js.map +0 -1
  151. package/lib/user-agent/transport/ws.js.map +0 -1
  152. package/lib/user-agent.js.map +0 -1
  153. package/lib/util.js.map +0 -1
  154. package/lib/validator/result.js.map +0 -1
  155. package/lib/validator.js.map +0 -1
  156. package/lib/websocket.js.map +0 -1
package/lib/context.d.ts CHANGED
@@ -1,15 +1,16 @@
1
- import type { App } from './app.js';
2
- import type { ChildLogger } from './logger.js';
3
- import type { Plan } from './router/plan.js';
4
- import type { ServerRequest } from './server/request.js';
5
- import type { ServerResponse } from './server/response.js';
6
- import type { BackendInfo, MojoAction, MojoModels, MojoRenderOptions, MojoURLOptions, ScopedNestedHelpers, SessionData, UploadOptions, ValidatorFunction } from './types.js';
7
- import type { UserAgent } from './user-agent.js';
8
- import type { WebSocket } from './websocket.js';
1
+ import type { App } from './app.ts';
2
+ import type { ChildLogger } from './logger.ts';
3
+ import type { Plan } from './router/plan.ts';
4
+ import type { ServerRequest } from './server/request.ts';
5
+ import type { ServerResponse } from './server/response.ts';
6
+ import type { BackendInfo, MojoAction, MojoModels, MojoRenderOptions, MojoURLOptions, ScopedNestedHelpers, SessionData, SSEEvent, UploadOptions, ValidatorFunction } from './types.ts';
7
+ import type { UserAgent } from './user-agent.ts';
8
+ import type { WebSocket } from './websocket.ts';
9
9
  import type Path from '@mojojs/path';
10
10
  import EventEmitter from 'node:events';
11
- import { Params } from './body/params.js';
12
- import { SafeString } from './util.js';
11
+ import { PassThrough } from 'node:stream';
12
+ import { Params } from './body/params.ts';
13
+ import { SafeString } from './util.ts';
13
14
  type WebSocketHandler = (ws: WebSocket) => void | Promise<void>;
14
15
  interface ContextEvents {
15
16
  connection: (ws: WebSocket) => void;
@@ -105,6 +106,7 @@ declare class Context extends EventEmitter implements ContextEventEmitter {
105
106
  _nestedHelpersCache: Record<string, ScopedNestedHelpers>;
106
107
  _params: Params | undefined;
107
108
  _session: Record<string, any> | undefined;
109
+ _sse: PassThrough | undefined;
108
110
  _ws: WeakRef<WebSocket> | null;
109
111
  constructor(app: App, req: ServerRequest, res: ServerResponse, backend: BackendInfo);
110
112
  [EventEmitter.captureRejectionSymbol](error: Error): void;
@@ -126,6 +128,10 @@ declare class Context extends EventEmitter implements ContextEventEmitter {
126
128
  * await ctx.contentFor('head', '<link rel="icon" href="/static/favicon.ico">');
127
129
  */
128
130
  contentFor(name: string, content: string | SafeString | Promise<string | SafeString> | (() => Promise<string | SafeString>)): Promise<void>;
131
+ /**
132
+ * Finish Server-Sent Events response. Note that this method is EXPERIMENTAL and might change without warning!
133
+ */
134
+ finishSSE(): void;
129
135
  /**
130
136
  * Data storage persistent only for the next request.
131
137
  * @example
@@ -286,6 +292,23 @@ declare class Context extends EventEmitter implements ContextEventEmitter {
286
292
  * Established WebSocket connection.
287
293
  */
288
294
  get ws(): WebSocket | null;
295
+ /**
296
+ * Write Server-Sent Event, and wait for it to be written. Calling this method without an event will just send the
297
+ * response headers and allow for events to be written later. Note that this method is EXPERIMENTAL and might change
298
+ * without warning!
299
+ * @example
300
+ * // Send event with default "message" type
301
+ * await ctx.writeSSE({text: 'Hello World!'});
302
+ *
303
+ * // You can send comments in regular intervals to keep the connection alive
304
+ * await ctx.writeSSE({comment: 'Keep connection alive!'});
305
+ *
306
+ * // Send events with id and type
307
+ * await ctx.writeSSE({id: 23, type: 'greeting', text: 'Hello World!'});
308
+ * await ctx.writeSSE({id: 24, type: 'farewell', text: 'Goodbye World!'});
309
+ * ctx.finishSSE();
310
+ */
311
+ writeSSE(event?: SSEEvent): Promise<void>;
289
312
  _urlForPath(path: string, isWebSocket: boolean, options: MojoURLOptions): string;
290
313
  }
291
314
  export { Context };
package/lib/context.js CHANGED
@@ -1,46 +1,110 @@
1
1
  import EventEmitter from 'node:events';
2
2
  import querystring from 'node:querystring';
3
- import { Params } from './body/params.js';
4
- import { SafeString } from './util.js';
3
+ import { PassThrough } from 'node:stream';
4
+ import { Params } from "./body/params.js";
5
+ import { buildEvent } from "./sse.js";
6
+ import { SafeString } from "./util.js";
5
7
  const ABSOLUTE = /^[a-zA-Z][a-zA-Z0-9]*:\/\//;
6
8
  /**
7
9
  * Context class.
8
10
  */
9
11
  class Context extends EventEmitter {
12
+ /**
13
+ * Application this context belongs to.
14
+ */
15
+ app;
16
+ /**
17
+ * Backend information for server that received the request.
18
+ * @example
19
+ * // Backend name (usually "server", "cgi", or "mock")
20
+ * const {name} = ctx.backend;
21
+ *
22
+ * // Use low level Node.js APIs with "server" backend
23
+ * const {res} = ctx.backend;
24
+ * res.writeHead(200);
25
+ * res.end('Hello World!');
26
+ */
27
+ backend;
28
+ /**
29
+ * Partial content. Always returns at least an empty string, even for sections that have not been defined with
30
+ * `ctx.contentFor()`.
31
+ * @example
32
+ * // Get content for "head" section
33
+ * const head = ctx.content.head;
34
+ */
35
+ content = new Proxy({}, {
36
+ get: function (target, name) {
37
+ return new SafeString(target[name] ?? '');
38
+ }
39
+ });
40
+ /**
41
+ * Format for HTTP exceptions ("html", "json", or "txt").
42
+ */
43
+ exceptionFormat;
44
+ /**
45
+ * WebSocket JSON mode.
46
+ */
47
+ jsonMode = false;
48
+ /**
49
+ * Logger with request id.
50
+ * @example
51
+ * // Pass logger with context to model
52
+ * const log = ctx.log;
53
+ * ctx.model.users.create({name: 'kraih'}, log);
54
+ */
55
+ log;
56
+ /**
57
+ * Router dispatch plan.
58
+ */
59
+ plan = null;
60
+ /**
61
+ * HTTP request information.
62
+ * @example
63
+ * // Extract request information
64
+ * const id = ctx.req.requestId;
65
+ * const method = ctx.req.method;
66
+ * const baseURL = ctx.req.baseURL;
67
+ * const url = ctx.req.url;
68
+ * const query = ctx.req.query;
69
+ * const info = ctx.req.userinfo;
70
+ * const agent = ctx.req.get('User-Agent');
71
+ * const buffer = await ctx.req.buffer();
72
+ * const text = await ctx.req.text();
73
+ * const form = await ctx.req.form();
74
+ * const value = await ctx.req.json();
75
+ * const html = await ctx.req.html();
76
+ * const xml = await ctx.req.xml();
77
+ */
78
+ req;
79
+ /**
80
+ * HTTP response information.
81
+ * @example
82
+ * // Force file download by setting a response header
83
+ * ctx.res.set('Content-Disposition', 'attachment; filename=foo.png;');
84
+ *
85
+ * // Make sure response is cached correctly
86
+ * ctx.res.set('Cache-Control', 'public, max-age=300');
87
+ * ctx.res.headers.append('Vary', 'Accept-Encoding');
88
+ */
89
+ res;
90
+ /**
91
+ * Non-persistent data storage and exchange for the current request.
92
+ */
93
+ stash = {};
94
+ _flash = undefined;
95
+ _nestedHelpersCache = {};
96
+ _params = undefined;
97
+ _session = undefined;
98
+ _sse = undefined;
99
+ _ws = null;
10
100
  constructor(app, req, res, backend) {
11
101
  super({ captureRejections: true });
12
- /**
13
- * Partial content. Always returns at least an empty string, even for sections that have not been defined with
14
- * `ctx.contentFor()`.
15
- * @example
16
- * // Get content for "head" section
17
- * const head = ctx.content.head;
18
- */
19
- this.content = new Proxy({}, {
20
- get: function (target, name) {
21
- return new SafeString(target[name] ?? '');
22
- }
23
- });
24
- /**
25
- * WebSocket JSON mode.
26
- */
27
- this.jsonMode = false;
28
- /**
29
- * Router dispatch plan.
30
- */
31
- this.plan = null;
32
- /**
33
- * Non-persistent data storage and exchange for the current request.
34
- */
35
- this.stash = {};
36
- this._flash = undefined;
37
- this._nestedHelpersCache = {};
38
- this._params = undefined;
39
- this._session = undefined;
40
- this._ws = null;
41
102
  this.app = app;
42
103
  this.exceptionFormat = app.exceptionFormat;
43
104
  this.req = req;
105
+ // Mounted applications share the request, and the limit of the outer one
106
+ if (req.maxMessageSize === undefined)
107
+ req.maxMessageSize = app.maxRequestSize;
44
108
  this.res = res;
45
109
  this.res.bindContext(this);
46
110
  this.backend = backend;
@@ -80,6 +144,12 @@ class Context extends EventEmitter {
80
144
  async contentFor(name, content) {
81
145
  this.content[name] += await (content instanceof Function ? content() : content);
82
146
  }
147
+ /**
148
+ * Finish Server-Sent Events response. Note that this method is EXPERIMENTAL and might change without warning!
149
+ */
150
+ finishSSE() {
151
+ this._sse?.end();
152
+ }
83
153
  /**
84
154
  * Data storage persistent only for the next request.
85
155
  * @example
@@ -361,6 +431,42 @@ class Context extends EventEmitter {
361
431
  get ws() {
362
432
  return this._ws?.deref() ?? null;
363
433
  }
434
+ /**
435
+ * Write Server-Sent Event, and wait for it to be written. Calling this method without an event will just send the
436
+ * response headers and allow for events to be written later. Note that this method is EXPERIMENTAL and might change
437
+ * without warning!
438
+ * @example
439
+ * // Send event with default "message" type
440
+ * await ctx.writeSSE({text: 'Hello World!'});
441
+ *
442
+ * // You can send comments in regular intervals to keep the connection alive
443
+ * await ctx.writeSSE({comment: 'Keep connection alive!'});
444
+ *
445
+ * // Send events with id and type
446
+ * await ctx.writeSSE({id: 23, type: 'greeting', text: 'Hello World!'});
447
+ * await ctx.writeSSE({id: 24, type: 'farewell', text: 'Goodbye World!'});
448
+ * ctx.finishSSE();
449
+ */
450
+ async writeSSE(event) {
451
+ let stream = this._sse;
452
+ if (stream === undefined) {
453
+ stream = this._sse = new PassThrough();
454
+ this.on('finish', () => stream?.destroy());
455
+ await this.res.type('text/event-stream').send(stream);
456
+ }
457
+ if (event === undefined || stream.destroyed === true)
458
+ return;
459
+ if (stream.write(buildEvent(event)) === true)
460
+ return;
461
+ // Wait for drain, or the connection to be closed
462
+ await new Promise(resolve => {
463
+ const done = () => {
464
+ stream.off('drain', done).off('close', done);
465
+ resolve();
466
+ };
467
+ stream.on('drain', done).on('close', done);
468
+ });
469
+ }
364
470
  _urlForPath(path, isWebSocket, options) {
365
471
  path = this.req.basePath + (path === '' ? '/' : path);
366
472
  let queryFragment = '';
@@ -376,4 +482,3 @@ class Context extends EventEmitter {
376
482
  }
377
483
  }
378
484
  export { Context };
379
- //# sourceMappingURL=context.js.map
package/lib/core.d.ts CHANGED
@@ -3,21 +3,22 @@
3
3
  * Copyright (C) 2021-2024 Sebastian Riedel
4
4
  * MIT Licensed
5
5
  */
6
- import type { AppOptions } from './types.js';
7
- import { App } from './app.js';
6
+ import type { AppOptions } from './types.ts';
7
+ import { App } from './app.ts';
8
8
  export declare const version: any;
9
9
  /**
10
10
  * Create a new mojo.js application.
11
11
  */
12
12
  export default function mojo(options?: AppOptions): App;
13
- export { default as jsonConfigPlugin } from './plugins/json-config.js';
14
- export { default as yamlConfigPlugin } from './plugins/yaml-config.js';
15
- export { default as mountPlugin } from './plugins/mount.js';
16
- export { CGI } from './cgi.js';
17
- export { Logger } from './logger.js';
18
- export { Server } from './server.js';
19
- export { Session } from './session.js';
20
- export { TestUserAgent } from './user-agent/test.js';
21
- export { UserAgent } from './user-agent.js';
22
- export * as util from './util.js';
23
- export { JSONValue, MojoAction, MojoApp, MojoContext, MojoModels, MojoRenderOptions, MojoRoute, MojoTags, MojoURLOptions } from './types.js';
13
+ export { default as jsonConfigPlugin } from './plugins/json-config.ts';
14
+ export { default as yamlConfigPlugin } from './plugins/yaml-config.ts';
15
+ export { default as mountPlugin } from './plugins/mount.ts';
16
+ export { CGI } from './cgi.ts';
17
+ export { Logger } from './logger.ts';
18
+ export { Server } from './server.ts';
19
+ export { Session } from './session.ts';
20
+ export { buildEvent, parseEvent } from './sse.ts';
21
+ export { TestUserAgent } from './user-agent/test.ts';
22
+ export { UserAgent } from './user-agent.ts';
23
+ export * as util from './util.ts';
24
+ export type { JSONValue, MojoAction, MojoApp, MojoContext, MojoModels, MojoRenderOptions, MojoRoute, MojoTags, MojoURLOptions, SSEEvent } from './types.ts';
package/lib/core.js CHANGED
@@ -1,4 +1,4 @@
1
- import { App } from './app.js';
1
+ import { App } from "./app.js";
2
2
  import Path from '@mojojs/path';
3
3
  export const version = JSON.parse(Path.currentFile().dirname().sibling('package.json').readFileSync().toString()).version;
4
4
  /**
@@ -22,14 +22,14 @@ export default function mojo(options) {
22
22
  return app;
23
23
  }
24
24
  // "Professor: These old Doomsday devices are dangerously unstable. I'll rest easier not knowing where they are."
25
- export { default as jsonConfigPlugin } from './plugins/json-config.js';
26
- export { default as yamlConfigPlugin } from './plugins/yaml-config.js';
27
- export { default as mountPlugin } from './plugins/mount.js';
28
- export { CGI } from './cgi.js';
29
- export { Logger } from './logger.js';
30
- export { Server } from './server.js';
31
- export { Session } from './session.js';
32
- export { TestUserAgent } from './user-agent/test.js';
33
- export { UserAgent } from './user-agent.js';
34
- export * as util from './util.js';
35
- //# sourceMappingURL=core.js.map
25
+ export { default as jsonConfigPlugin } from "./plugins/json-config.js";
26
+ export { default as yamlConfigPlugin } from "./plugins/yaml-config.js";
27
+ export { default as mountPlugin } from "./plugins/mount.js";
28
+ export { CGI } from "./cgi.js";
29
+ export { Logger } from "./logger.js";
30
+ export { Server } from "./server.js";
31
+ export { Session } from "./session.js";
32
+ export { buildEvent, parseEvent } from "./sse.js";
33
+ export { TestUserAgent } from "./user-agent/test.js";
34
+ export { UserAgent } from "./user-agent.js";
35
+ export * as util from "./util.js";
package/lib/headers.js CHANGED
@@ -13,6 +13,8 @@ const HOP_BY_HOP = [
13
13
  * HTTP header class.
14
14
  */
15
15
  export class Headers {
16
+ _headers;
17
+ _init;
16
18
  constructor(init = []) {
17
19
  this._init = init;
18
20
  }
@@ -70,7 +72,6 @@ export class Headers {
70
72
  * const {link, title} = headers.getLinks().next;
71
73
  */
72
74
  getLinks() {
73
- var _a;
74
75
  const data = {};
75
76
  let value = this.get('Link') ?? '';
76
77
  while (value.length > 0) {
@@ -81,7 +82,7 @@ export class Headers {
81
82
  const { params, remainder } = headerParams(linkMatch[2]);
82
83
  value = remainder;
83
84
  if (params.rel !== undefined)
84
- data[_a = params.rel] ?? (data[_a] = { ...params, link });
85
+ data[params.rel] ??= { ...params, link };
85
86
  }
86
87
  return data;
87
88
  }
@@ -149,7 +150,8 @@ export class Headers {
149
150
  }
150
151
  _getHeaders() {
151
152
  if (this._headers === undefined) {
152
- const headers = (this._headers = {});
153
+ // No prototype, so names like "constructor" are just regular headers
154
+ const headers = (this._headers = Object.create(null));
153
155
  const init = this._init;
154
156
  for (let i = 0; i < init.length; i += 2) {
155
157
  const name = init[i];
@@ -166,4 +168,3 @@ export class Headers {
166
168
  return this._headers;
167
169
  }
168
170
  }
169
- //# sourceMappingURL=headers.js.map
package/lib/hooks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MojoApp } from './types.js';
1
+ import type { MojoApp } from './types.ts';
2
2
  import { AsyncHooks } from '@mojojs/util';
3
3
  /**
4
4
  * Hook class.
package/lib/hooks.js CHANGED
@@ -3,10 +3,7 @@ import { AsyncHooks } from '@mojojs/util';
3
3
  * Hook class.
4
4
  */
5
5
  export class Hooks extends AsyncHooks {
6
- constructor() {
7
- super(...arguments);
8
- this._lifecycleHookScore = 0;
9
- }
6
+ _lifecycleHookScore = 0;
10
7
  /**
11
8
  * Run `command:before` hook with dependencies. Note that this method is EXPERIMENTAL and might change without
12
9
  * warning!
@@ -49,4 +46,3 @@ export class Hooks extends AsyncHooks {
49
46
  await this.runHook('app:stop', app);
50
47
  }
51
48
  }
52
- //# sourceMappingURL=hooks.js.map
package/lib/logger.d.ts CHANGED
@@ -8,7 +8,7 @@ interface LogEvent extends LogContext {
8
8
  time: string;
9
9
  msg: string;
10
10
  }
11
- type LogFormatter = (data: LogEvent) => string;
11
+ type LogFormatter = (data: LogEvent, destination: NodeJS.WritableStream) => string;
12
12
  /**
13
13
  * A simple logger class.
14
14
  */
@@ -45,7 +45,7 @@ export declare class Logger {
45
45
  /**
46
46
  * Log formatter with color highlighting.
47
47
  */
48
- static colorFormatter(data: LogEvent): string;
48
+ static colorFormatter(data: LogEvent, destination?: NodeJS.WritableStream): string;
49
49
  /**
50
50
  * Log `debug` message.
51
51
  */
package/lib/logger.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert';
2
- import chalk from 'chalk';
2
+ import { styleText } from 'node:util';
3
3
  const LEVEL = {
4
4
  fatal: 2,
5
5
  error: 3,
@@ -12,13 +12,22 @@ const LEVEL = {
12
12
  * A simple logger class.
13
13
  */
14
14
  export class Logger {
15
+ /**
16
+ * Log destination stream.
17
+ */
18
+ destination;
19
+ /**
20
+ * Log formatter.
21
+ */
22
+ formatter;
23
+ /**
24
+ * The last few logged messages.
25
+ */
26
+ history = [];
27
+ _capture = undefined;
28
+ _historySize;
29
+ _level = 7;
15
30
  constructor(options = {}) {
16
- /**
17
- * The last few logged messages.
18
- */
19
- this.history = [];
20
- this._capture = undefined;
21
- this._level = 7;
22
31
  this.destination = options.destination ?? process.stderr;
23
32
  this.formatter = options.formatter ?? Logger.colorFormatter;
24
33
  this.level = process.env.MOJO_LOG_LEVEL ?? options.level ?? 'trace';
@@ -47,13 +56,14 @@ export class Logger {
47
56
  /**
48
57
  * Log formatter with color highlighting.
49
58
  */
50
- static colorFormatter(data) {
59
+ static colorFormatter(data, destination = process.stderr) {
51
60
  const formatted = Logger.stringFormatter(data);
52
- if (data.level === 'error' || data.level === 'fatal')
53
- return chalk.red(formatted);
54
- if (data.level === 'warn')
55
- return chalk.yellow(formatted);
56
- return formatted;
61
+ const { level } = data;
62
+ const color = level === 'error' || level === 'fatal' ? 'red' : level === 'warn' ? 'yellow' : null;
63
+ if (color === null)
64
+ return formatted;
65
+ // Keep the trailing newline outside of the color codes, and only use colors if the destination supports them
66
+ return styleText(color, formatted.slice(0, -1), { stream: destination }) + '\n';
57
67
  }
58
68
  /**
59
69
  * Log `debug` message.
@@ -139,7 +149,7 @@ export class Logger {
139
149
  if (historySize > maxHistorySize)
140
150
  history.splice(0, historySize - maxHistorySize);
141
151
  }
142
- const formatted = this.formatter(data);
152
+ const formatted = this.formatter(data, this.destination);
143
153
  if (this._capture === undefined) {
144
154
  this.destination.write(formatted);
145
155
  }
@@ -152,9 +162,10 @@ export class Logger {
152
162
  * Captured log message class.
153
163
  */
154
164
  class CapturedLogs extends Array {
165
+ _cb;
166
+ _stopped = false;
155
167
  constructor(cb) {
156
168
  super();
157
- this._stopped = false;
158
169
  this._cb = cb;
159
170
  }
160
171
  /**
@@ -177,6 +188,8 @@ class CapturedLogs extends Array {
177
188
  * Child logger class.
178
189
  */
179
190
  export class ChildLogger {
191
+ parent;
192
+ context;
180
193
  constructor(parent, context) {
181
194
  this.parent = parent;
182
195
  this.context = context;
@@ -218,4 +231,3 @@ export class ChildLogger {
218
231
  this.parent.warn(msg, this.context);
219
232
  }
220
233
  }
221
- //# sourceMappingURL=logger.js.map
package/lib/mime.js CHANGED
@@ -3,18 +3,16 @@ import mime from 'mime-types';
3
3
  * MIME class.
4
4
  */
5
5
  export class Mime {
6
- constructor() {
7
- /**
8
- * Custom MIME types.
9
- */
10
- this.custom = {
11
- html: 'text/html; charset=utf-8',
12
- json: 'application/json; charset=utf-8',
13
- txt: 'text/plain; charset=utf-8',
14
- yaml: 'text/yaml; charset=utf-8'
15
- };
16
- this._reverseCustom = undefined;
17
- }
6
+ /**
7
+ * Custom MIME types.
8
+ */
9
+ custom = {
10
+ html: 'text/html; charset=utf-8',
11
+ json: 'application/json; charset=utf-8',
12
+ txt: 'text/plain; charset=utf-8',
13
+ yaml: 'text/yaml; charset=utf-8'
14
+ };
15
+ _reverseCustom = undefined;
18
16
  /**
19
17
  * Detect file extensions from `Accept` header value.
20
18
  */
@@ -50,4 +48,3 @@ export class Mime {
50
48
  return this.custom[ext] ?? mime.types[ext] ?? null;
51
49
  }
52
50
  }
53
- //# sourceMappingURL=mime.js.map
@@ -1,4 +1,4 @@
1
- import type { App } from '../app.js';
1
+ import type { App } from '../app.ts';
2
2
  /**
3
3
  * Default condition plugin.
4
4
  */
@@ -19,4 +19,3 @@ async function headerCondition(ctx, requirement) {
19
19
  async function hostCondition(ctx, requirement) {
20
20
  return headerCondition(ctx, { Host: requirement });
21
21
  }
22
- //# sourceMappingURL=default-conditions.js.map
@@ -1,4 +1,4 @@
1
- import type { MojoApp } from '../types.js';
1
+ import type { MojoApp } from '../types.ts';
2
2
  /**
3
3
  * Default helper plugin.
4
4
  */
@@ -1,6 +1,6 @@
1
1
  import { inspect } from 'node:util';
2
- import { Logger } from '../logger.js';
3
- import { exceptionContext, SafeString } from '../util.js';
2
+ import { Logger } from "../logger.js";
3
+ import { exceptionContext, HTTPError, httpStatusMessages, SafeString } from "../util.js";
4
4
  import DOM from '@mojojs/dom';
5
5
  /**
6
6
  * Default helper plugin.
@@ -140,12 +140,13 @@ async function hiddenFieldTag(ctx, name, value, attrs = {}) {
140
140
  async function htmlException(ctx, error) {
141
141
  ctx.stash.exception = error;
142
142
  const mode = ctx.app.mode;
143
- if (await ctx.render({ view: `exception.${mode}`, maybe: true, status: 500 }))
143
+ const status = errorStatus(error);
144
+ if (await ctx.render({ view: `exception.${mode}`, maybe: true, status }))
144
145
  return true;
145
- if (await ctx.render({ view: 'exception', maybe: true, status: 500 }))
146
+ if (await ctx.render({ view: 'exception', maybe: true, status }))
146
147
  return true;
147
148
  const view = mode === 'development' ? 'mojo/debug' : 'mojo/exception';
148
- return await ctx.render({ view, status: 500, stringFormatter: Logger.stringFormatter, exceptionContext });
149
+ return await ctx.render({ view, status, stringFormatter: Logger.stringFormatter, exceptionContext });
149
150
  }
150
151
  async function htmlNotFound(ctx) {
151
152
  ctx.stash.exception = null;
@@ -159,7 +160,15 @@ async function htmlNotFound(ctx) {
159
160
  }
160
161
  async function httpException(ctx, error) {
161
162
  error = ensureError(error);
162
- ctx.log.error(error.stack);
163
+ // Client errors are not worth a stack trace
164
+ const status = errorStatus(error);
165
+ if (status < 500)
166
+ ctx.log.debug(error.message);
167
+ else
168
+ ctx.log.error(error.stack);
169
+ // Do not wait for the rest of a request body that is too large
170
+ if (status === 413)
171
+ ctx.res.set('Connection', 'close');
163
172
  const exceptionFormat = ctx.exceptionFormat;
164
173
  if (exceptionFormat === 'txt')
165
174
  return ctx.txtException(error);
@@ -195,6 +204,7 @@ async function include(ctx, options, stash) {
195
204
  return str === null ? null : new SafeString(str);
196
205
  }
197
206
  async function jsonException(ctx, error) {
207
+ const status = errorStatus(error);
198
208
  if (ctx.app.mode === 'development') {
199
209
  return await ctx.render({
200
210
  json: {
@@ -205,10 +215,10 @@ async function jsonException(ctx, error) {
205
215
  }
206
216
  },
207
217
  pretty: true,
208
- status: 500
218
+ status
209
219
  });
210
220
  }
211
- return await ctx.render({ json: { error: { message: 'Internal Server Error' } }, pretty: true, status: 500 });
221
+ return await ctx.render({ json: { error: { message: httpStatusMessages[status] } }, pretty: true, status });
212
222
  }
213
223
  async function jsonNotFound(ctx) {
214
224
  return await ctx.render({ json: { error: { message: 'Not Found' } }, pretty: true, status: 404 });
@@ -276,10 +286,11 @@ async function textAreaTag(ctx, name, attrs = {}, content) {
276
286
  return ctx.tag('textarea', { ...attrs, name }, content);
277
287
  }
278
288
  async function txtException(ctx, error) {
289
+ const status = errorStatus(error);
279
290
  if (ctx.app.mode === 'development') {
280
- return await ctx.render({ text: error.stack, status: 500 });
291
+ return await ctx.render({ text: error.stack, status });
281
292
  }
282
- return await ctx.render({ text: 'Internal Server Error', status: 500 });
293
+ return await ctx.render({ text: httpStatusMessages[status], status });
283
294
  }
284
295
  async function txtNotFound(ctx) {
285
296
  return await ctx.render({ text: 'Not Found', status: 404 });
@@ -299,4 +310,10 @@ function urlTarget(target) {
299
310
  function ensureError(error) {
300
311
  return error instanceof Error ? error : new Error(error);
301
312
  }
302
- //# sourceMappingURL=default-helpers.js.map
313
+ // Only our own errors, a "status" property from other libraries could hide real server errors
314
+ function errorStatus(error) {
315
+ if (!(error instanceof HTTPError))
316
+ return 500;
317
+ const { status } = error;
318
+ return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
319
+ }
@@ -1,4 +1,4 @@
1
- import type { ConfigOptions, MojoApp } from '../types.js';
1
+ import type { ConfigOptions, MojoApp } from '../types.ts';
2
2
  /**
3
3
  * JSON config plugin.
4
4
  */