fulmine.js 5.0.0-rc.1
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/EXPRESS_LICENSE +26 -0
- package/LICENSE +202 -0
- package/NOTICE +38 -0
- package/README.md +469 -0
- package/package.json +165 -0
- package/src/application.js +561 -0
- package/src/cli.js +369 -0
- package/src/declarative.js +768 -0
- package/src/index.js +71 -0
- package/src/middlewares.js +636 -0
- package/src/node-shim.js +400 -0
- package/src/request.js +807 -0
- package/src/response.js +1360 -0
- package/src/router.js +1240 -0
- package/src/types.d.ts +62 -0
- package/src/utils.js +993 -0
- package/src/view.js +172 -0
- package/src/worker.js +38 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
|
|
19
|
+
// declaration file the package ships, so the module is read through a loose alias
|
|
20
|
+
const uWS = require("uWebSockets.js");
|
|
21
|
+
const uWSAny = /** @type {any} */ (uWS);
|
|
22
|
+
const Router = require("./router.js");
|
|
23
|
+
const {
|
|
24
|
+
removeDuplicateSlashes,
|
|
25
|
+
defaultSettings,
|
|
26
|
+
compileTrust,
|
|
27
|
+
createETagGenerator,
|
|
28
|
+
fastQueryParse,
|
|
29
|
+
NullObject
|
|
30
|
+
} = require("./utils.js");
|
|
31
|
+
const querystring = require("fast-querystring");
|
|
32
|
+
const ViewClass = require("./view.js");
|
|
33
|
+
const path = require("path");
|
|
34
|
+
const os = require("os");
|
|
35
|
+
const { Worker } = require("worker_threads");
|
|
36
|
+
const cluster = require("cluster");
|
|
37
|
+
|
|
38
|
+
const cpuCount = os.cpus().length;
|
|
39
|
+
|
|
40
|
+
const workers = [];
|
|
41
|
+
let taskKey = 0;
|
|
42
|
+
const workerTasks = new NullObject();
|
|
43
|
+
|
|
44
|
+
class FSWorker {
|
|
45
|
+
/**
|
|
46
|
+
* A worker thread that does nothing but read files, so a read does not sit on the event loop.
|
|
47
|
+
* It is unref'd, so an idle one does not keep the process alive, and it is shared between every
|
|
48
|
+
* app in the process rather than started per app.
|
|
49
|
+
*/
|
|
50
|
+
constructor() {
|
|
51
|
+
this.busy = false;
|
|
52
|
+
this.worker = new Worker(path.join(__dirname, "worker.js"));
|
|
53
|
+
|
|
54
|
+
this.worker.on("message", (message) => {
|
|
55
|
+
this.busy = false;
|
|
56
|
+
if (message.err) {
|
|
57
|
+
workerTasks[message.key].reject(new Error(message.err));
|
|
58
|
+
} else {
|
|
59
|
+
// worker transfers file contents as an ArrayBuffer; wrap it in a Buffer (zero-copy) so
|
|
60
|
+
// consumers get the same type as fs.readFile. A bare ArrayBuffer is rejected by wrapped
|
|
61
|
+
// res.end() implementations (e.g. express-session calls Buffer.byteLength on the chunk).
|
|
62
|
+
workerTasks[message.key].resolve(
|
|
63
|
+
message.data instanceof ArrayBuffer ? Buffer.from(message.data) : message.data
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
delete workerTasks[message.key];
|
|
67
|
+
});
|
|
68
|
+
this.worker.unref();
|
|
69
|
+
|
|
70
|
+
workers.push(this);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class Application extends Router {
|
|
75
|
+
/**
|
|
76
|
+
* @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
|
|
77
|
+
* between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
|
|
78
|
+
* turns it off; uwsApp adopts an existing uWS app instead of making one. Everything else is
|
|
79
|
+
* an application setting and lands next to the defaults.
|
|
80
|
+
*/
|
|
81
|
+
constructor(settings = new NullObject()) {
|
|
82
|
+
super(settings);
|
|
83
|
+
if (!settings?.uwsOptions) {
|
|
84
|
+
settings.uwsOptions = {};
|
|
85
|
+
}
|
|
86
|
+
if (typeof settings.threads !== "number") {
|
|
87
|
+
settings.threads = cpuCount > 1 ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
if (settings.uwsApp) {
|
|
90
|
+
this.uwsApp = settings.uwsApp;
|
|
91
|
+
} else if (settings.http3) {
|
|
92
|
+
if (!settings.uwsOptions.key_file_name || !settings.uwsOptions.cert_file_name) {
|
|
93
|
+
throw new Error("uwsOptions.key_file_name and uwsOptions.cert_file_name are required for HTTP/3");
|
|
94
|
+
}
|
|
95
|
+
this.uwsApp = uWSAny.H3App(settings.uwsOptions);
|
|
96
|
+
} else if (settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name) {
|
|
97
|
+
this.uwsApp = uWS.SSLApp(settings.uwsOptions);
|
|
98
|
+
} else {
|
|
99
|
+
this.uwsApp = uWS.App(settings.uwsOptions);
|
|
100
|
+
}
|
|
101
|
+
this.ssl = settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name;
|
|
102
|
+
this.cache = new NullObject();
|
|
103
|
+
this.engines = { __proto__: null };
|
|
104
|
+
this.locals = {
|
|
105
|
+
settings: this.settings
|
|
106
|
+
};
|
|
107
|
+
this.listenCalled = false;
|
|
108
|
+
this.workers = [];
|
|
109
|
+
for (let i = 0; i < settings.threads; i++) {
|
|
110
|
+
if (workers[i]) {
|
|
111
|
+
this.workers[i] = workers[i];
|
|
112
|
+
} else {
|
|
113
|
+
this.workers[i] = new FSWorker();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.port = undefined;
|
|
117
|
+
this.listening = false;
|
|
118
|
+
// the host handed to listen(), which is all address() has to go on
|
|
119
|
+
this._listenHost = undefined;
|
|
120
|
+
for (const key in defaultSettings) {
|
|
121
|
+
if (typeof this.settings[key] === "undefined") {
|
|
122
|
+
if (typeof defaultSettings[key] === "function") {
|
|
123
|
+
this.settings[key] = defaultSettings[key](this);
|
|
124
|
+
} else {
|
|
125
|
+
this.settings[key] = defaultSettings[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.set("view", ViewClass);
|
|
130
|
+
this.set("views", path.resolve("views"));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parks a promise's settle functions under a key the worker can send back, since a worker
|
|
135
|
+
* message carries data and not closures. The counter wraps rather than growing without bound,
|
|
136
|
+
* a million tasks being far more than can be outstanding at once.
|
|
137
|
+
*
|
|
138
|
+
* @param {(value: any) => void} resolve
|
|
139
|
+
* @param {(err: any) => void} reject
|
|
140
|
+
* @returns {number} the key to send to the worker
|
|
141
|
+
*/
|
|
142
|
+
createWorkerTask(resolve, reject) {
|
|
143
|
+
const key = taskKey++;
|
|
144
|
+
workerTasks[key] = { resolve, reject };
|
|
145
|
+
if (key > 1000000) {
|
|
146
|
+
taskKey = 0;
|
|
147
|
+
}
|
|
148
|
+
return key;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reads a file on one of the file threads, picked at random, rather than on the event loop.
|
|
153
|
+
* Only worth it below the size where the copy back costs more than the read, which is why
|
|
154
|
+
* res.sendFile uses it for small files and streams the rest.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} path absolute path to read
|
|
157
|
+
* @returns {Promise<Buffer>}
|
|
158
|
+
*/
|
|
159
|
+
readFileWithWorker(path) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const worker = this.workers[Math.floor(Math.random() * this.workers.length)];
|
|
162
|
+
const key = this.createWorkerTask(resolve, reject);
|
|
163
|
+
worker.busy = true;
|
|
164
|
+
worker.worker.postMessage({ key, type: "readFile", path });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reads or writes an application setting. One argument is the getter, and the check is on
|
|
170
|
+
* `arguments.length`, so `set(key, undefined)` still writes. Some keys have a side effect:
|
|
171
|
+
* `trust proxy`, `query parser` and `etag` compile the value into a function kept beside it,
|
|
172
|
+
* `views` becomes an absolute path, and `env` set to "production" turns the view cache on.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} key setting name
|
|
175
|
+
* @param {*} [value] value to store; omit to read instead
|
|
176
|
+
* @returns {*} the app, for chaining, or the value when reading
|
|
177
|
+
*/
|
|
178
|
+
set(key, value) {
|
|
179
|
+
if (arguments.length === 1) {
|
|
180
|
+
return this.get(key);
|
|
181
|
+
}
|
|
182
|
+
if (key === "trust proxy") {
|
|
183
|
+
if (!value) {
|
|
184
|
+
delete this.settings["trust proxy fn"];
|
|
185
|
+
} else {
|
|
186
|
+
this.settings["trust proxy fn"] = compileTrust(value);
|
|
187
|
+
}
|
|
188
|
+
} else if (key === "query parser") {
|
|
189
|
+
if (value === "extended") {
|
|
190
|
+
this.settings["query parser fn"] = fastQueryParse;
|
|
191
|
+
} else if (value === "simple") {
|
|
192
|
+
this.settings["query parser fn"] = querystring.parse;
|
|
193
|
+
} else if (typeof value === "function") {
|
|
194
|
+
this.settings["query parser fn"] = value;
|
|
195
|
+
} else {
|
|
196
|
+
this.settings["query parser fn"] = undefined;
|
|
197
|
+
}
|
|
198
|
+
} else if (key === "env") {
|
|
199
|
+
if (value === "production") {
|
|
200
|
+
this.settings["view cache"] = true;
|
|
201
|
+
} else {
|
|
202
|
+
this.settings["view cache"] = undefined;
|
|
203
|
+
}
|
|
204
|
+
} else if (key === "views") {
|
|
205
|
+
this.settings[key] = path.resolve(value);
|
|
206
|
+
return this;
|
|
207
|
+
} else if (key === "etag") {
|
|
208
|
+
if (typeof value === "function") {
|
|
209
|
+
this.settings["etag fn"] = value;
|
|
210
|
+
} else {
|
|
211
|
+
switch (value) {
|
|
212
|
+
case true:
|
|
213
|
+
case "weak":
|
|
214
|
+
this.settings["etag fn"] = createETagGenerator({ weak: true });
|
|
215
|
+
break;
|
|
216
|
+
case "strong":
|
|
217
|
+
this.settings["etag fn"] = createETagGenerator({ weak: false });
|
|
218
|
+
break;
|
|
219
|
+
case false:
|
|
220
|
+
delete this.settings["etag fn"];
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
throw new Error(`Invalid etag mode: ${value}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.settings[key] = value;
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Sets a setting to true, side effects and all.
|
|
234
|
+
* @param {string} key setting name
|
|
235
|
+
* @returns {this} the app, for chaining
|
|
236
|
+
*/
|
|
237
|
+
enable(key) {
|
|
238
|
+
this.set(key, true);
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Sets a setting to false, side effects and all.
|
|
244
|
+
* @param {string} key setting name
|
|
245
|
+
* @returns {this} the app, for chaining
|
|
246
|
+
*/
|
|
247
|
+
disable(key) {
|
|
248
|
+
this.set(key, false);
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether a setting is truthy. Reads this app's own settings, without falling back to a
|
|
254
|
+
* parent app the way get() does.
|
|
255
|
+
* @param {string} key setting name
|
|
256
|
+
* @returns {boolean}
|
|
257
|
+
*/
|
|
258
|
+
enabled(key) {
|
|
259
|
+
return !!this.settings[key];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Whether a setting is falsy.
|
|
264
|
+
* @param {string} key setting name
|
|
265
|
+
* @returns {boolean}
|
|
266
|
+
*/
|
|
267
|
+
disabled(key) {
|
|
268
|
+
return !this.settings[key];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Registers the catch-all uWS handler, which is what serves every request that no optimized
|
|
273
|
+
* route took natively. It walks this app's own chain and, when nothing in it answered, decides
|
|
274
|
+
* between an error, the automatic OPTIONS reply and a 404.
|
|
275
|
+
*/
|
|
276
|
+
_createRequestHandler() {
|
|
277
|
+
this.uwsApp.any("/*", async (res, req) => {
|
|
278
|
+
const { request, response } = this.handleRequest(res, req);
|
|
279
|
+
|
|
280
|
+
const matchedRoute = await this._routeRequest(request, response);
|
|
281
|
+
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
282
|
+
this._endUnmatched(request, response);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Binds the server and starts accepting requests.
|
|
289
|
+
*
|
|
290
|
+
* Returns the app and not an `http.Server`, since there is no node server underneath. The app
|
|
291
|
+
* carries `address()`, `close()`, `listening` and the 'listening' and 'close' events; anything
|
|
292
|
+
* needing a real server, socket.io being the usual case, wants `app.uwsApp`. The callback runs
|
|
293
|
+
* on the next tick with the bind error, if there was one. A path instead of a port is a unix
|
|
294
|
+
* socket.
|
|
295
|
+
*
|
|
296
|
+
* @param {number|string} [port] port, or a unix socket path; 0 picks a free port
|
|
297
|
+
* @param {string} [host] interface to bind; every interface when omitted
|
|
298
|
+
* @param {(err?: Error) => void} [callback] called once bound, or with the bind error
|
|
299
|
+
* @returns {this} the app, which doubles as the server handle
|
|
300
|
+
*/
|
|
301
|
+
listen(port, host, callback) {
|
|
302
|
+
this._compileOptimizedRoutes();
|
|
303
|
+
this._createRequestHandler();
|
|
304
|
+
// support listen(callback)
|
|
305
|
+
if (!callback && typeof port === "function") {
|
|
306
|
+
callback = port;
|
|
307
|
+
port = 0;
|
|
308
|
+
}
|
|
309
|
+
// support listen(port, callback)
|
|
310
|
+
if (typeof host === "function") {
|
|
311
|
+
callback = host;
|
|
312
|
+
host = undefined;
|
|
313
|
+
}
|
|
314
|
+
// uWS runs this handler from inside its own listen(), so everything it hands back to the
|
|
315
|
+
// caller is deferred a tick. Express binds synchronously too but reports through events,
|
|
316
|
+
// and node emits both 'listening' and 'error' from a process.nextTick.
|
|
317
|
+
const onListen = (socket) => {
|
|
318
|
+
if (!socket) {
|
|
319
|
+
/** @type {NodeJS.ErrnoException} */
|
|
320
|
+
const err = new Error("listen EADDRINUSE: address already in use :::" + port);
|
|
321
|
+
err.code = "EADDRINUSE";
|
|
322
|
+
// Express 5 registers the listen callback on 'error' as well as on 'listening',
|
|
323
|
+
// so a failed bind arrives at the callback rather than being thrown past it
|
|
324
|
+
if (callback) {
|
|
325
|
+
return process.nextTick(() => callback.call(this, err));
|
|
326
|
+
}
|
|
327
|
+
// no callback means no 'error' listener either, and an EventEmitter carrying an
|
|
328
|
+
// unhandled error rethrows it from the tick that emitted it, not from listen()
|
|
329
|
+
return process.nextTick(() => {
|
|
330
|
+
throw err;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
// the port is known synchronously, as it is in Express, so address() works as soon as
|
|
334
|
+
// listen() returns. The callback is not: running it here would run it before listen()
|
|
335
|
+
// had returned, and `const server = app.listen(p, () => server.address())` - the form
|
|
336
|
+
// the Express docs use - would die on the temporal dead zone.
|
|
337
|
+
this.port = uWS.us_socket_local_port(socket);
|
|
338
|
+
this.listening = true;
|
|
339
|
+
this._listenHost = host;
|
|
340
|
+
process.nextTick(() => {
|
|
341
|
+
// `this` is the app, which is what listen() returns here. Express binds it to the
|
|
342
|
+
// http.Server, which is what listen() returns there, so
|
|
343
|
+
// `function () { this.address() }` reads the same on both.
|
|
344
|
+
// The callback goes first: in Express it is registered as a 'listening' listener
|
|
345
|
+
// before the caller can add any of their own.
|
|
346
|
+
if (callback) callback.call(this);
|
|
347
|
+
this.emit("listening");
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
let fn = "listen";
|
|
351
|
+
const args = [];
|
|
352
|
+
// 1 = exclusive port, 0 = shared port
|
|
353
|
+
const uwsOptions = cluster.isPrimary ? 1 : 0;
|
|
354
|
+
if (typeof port !== "number") {
|
|
355
|
+
if (!isNaN(Number(port))) {
|
|
356
|
+
port = Number(port);
|
|
357
|
+
args.push(port, uwsOptions, onListen);
|
|
358
|
+
if (host) {
|
|
359
|
+
args.unshift(host);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
fn = "listen_unix";
|
|
363
|
+
args.push(onListen, port);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
args.push(port, uwsOptions, onListen);
|
|
367
|
+
if (host) {
|
|
368
|
+
args.unshift(host);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.listenCalled = true;
|
|
372
|
+
this.uwsApp[fn](...args);
|
|
373
|
+
return this;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The bound address, or null when not listening.
|
|
378
|
+
* @returns {{address: string, family: string, port: number}|null}
|
|
379
|
+
*/
|
|
380
|
+
address() {
|
|
381
|
+
if (!this.listening || !this.port) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
// uWS hands back the port and nothing else, so the address reported is the one we asked
|
|
385
|
+
// it to bind. No host means every interface, which node reports as "::". A hostname is
|
|
386
|
+
// reported as written, since what it resolved to is not readable back from here: node
|
|
387
|
+
// would say "::1" where this says "localhost".
|
|
388
|
+
const host = this._listenHost;
|
|
389
|
+
if (!host) {
|
|
390
|
+
return { address: "::", family: "IPv6", port: this.port };
|
|
391
|
+
}
|
|
392
|
+
return { address: host, family: host.includes(":") ? "IPv6" : "IPv4", port: this.port };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The full mount path of this app, walking up through every parent it is mounted on.
|
|
397
|
+
* A top level app returns the empty string rather than "/".
|
|
398
|
+
* @returns {string}
|
|
399
|
+
*/
|
|
400
|
+
path() {
|
|
401
|
+
const paths = [this.mountpath];
|
|
402
|
+
let parent = this.parent;
|
|
403
|
+
while (parent) {
|
|
404
|
+
paths.unshift(parent.mountpath);
|
|
405
|
+
parent = parent.parent;
|
|
406
|
+
}
|
|
407
|
+
const path = removeDuplicateSlashes(paths.join(""));
|
|
408
|
+
return path === "/" ? "" : path;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Registers a template engine for a file extension.
|
|
413
|
+
*
|
|
414
|
+
* The leading dot is optional: "pug" and ".pug" register the same thing.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} ext file extension the engine handles
|
|
417
|
+
* @param {(path: string, options: object, callback: (err: Error|null, rendered?: string) => void) => void} fn
|
|
418
|
+
* the engine, in the callback style consolidate-style engines use
|
|
419
|
+
* @returns {this} the app, for chaining
|
|
420
|
+
* @throws {Error} if fn is not a function
|
|
421
|
+
*/
|
|
422
|
+
engine(ext, fn) {
|
|
423
|
+
if (typeof fn !== "function") {
|
|
424
|
+
throw new Error("callback function required");
|
|
425
|
+
}
|
|
426
|
+
const extension = ext[0] !== "." ? "." + ext : ext;
|
|
427
|
+
this.engines[extension] = fn;
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Renders a view and hands the result to the callback, without sending anything.
|
|
433
|
+
* `res.render()` is the one that responds.
|
|
434
|
+
*
|
|
435
|
+
* `app.locals` and `options._locals` are merged into the options, in that order, so a
|
|
436
|
+
* per-request local wins over an application-wide one. Caching follows the "view cache"
|
|
437
|
+
* setting unless `options.cache` says otherwise.
|
|
438
|
+
*
|
|
439
|
+
* A function in the options position is taken as the callback.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} name view name, resolved against the "views" setting
|
|
442
|
+
* @param {Record<string, any>} [options] locals for the view
|
|
443
|
+
* @param {(err: Error|null, html?: string) => void} [callback] receives the rendered view. It
|
|
444
|
+
* is what render is for, so leaving it out throws, as it does in Express
|
|
445
|
+
*/
|
|
446
|
+
render(name, options, callback) {
|
|
447
|
+
if (typeof options === "function") {
|
|
448
|
+
callback = /** @type {any} */ (options);
|
|
449
|
+
options = new NullObject();
|
|
450
|
+
}
|
|
451
|
+
// render exists to hand the result somewhere, so there is always a callback by this point:
|
|
452
|
+
// either the third argument or the second one, shuffled above
|
|
453
|
+
const done = /** @type {(err: Error|null, html?: string) => void} */ (callback);
|
|
454
|
+
if (!options) {
|
|
455
|
+
options = new NullObject();
|
|
456
|
+
} else {
|
|
457
|
+
options = Object.assign({}, options);
|
|
458
|
+
}
|
|
459
|
+
for (const key in this.locals) {
|
|
460
|
+
options[key] = this.locals[key];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (options._locals) {
|
|
464
|
+
for (const key in options._locals) {
|
|
465
|
+
options[key] = options._locals[key];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (options.cache == null) {
|
|
470
|
+
options.cache = this.enabled("view cache");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let view;
|
|
474
|
+
if (options.cache) {
|
|
475
|
+
view = this.cache[name];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!view) {
|
|
479
|
+
const View = this.get("view");
|
|
480
|
+
view = new View(name, {
|
|
481
|
+
defaultEngine: this.get("view engine"),
|
|
482
|
+
root: this.get("views"),
|
|
483
|
+
engines: { ...this.engines }
|
|
484
|
+
});
|
|
485
|
+
if (!view.path) {
|
|
486
|
+
const dirs =
|
|
487
|
+
Array.isArray(view.root) && view.root.length > 1
|
|
488
|
+
? 'directories "' +
|
|
489
|
+
view.root.slice(0, -1).join('", "') +
|
|
490
|
+
'" or "' +
|
|
491
|
+
view.root[view.root.length - 1] +
|
|
492
|
+
'"'
|
|
493
|
+
: 'directory "' + view.root + '"';
|
|
494
|
+
|
|
495
|
+
/** @type {Error & { view?: unknown }} */
|
|
496
|
+
const err = new Error(`Failed to lookup view "${name}" in views ${dirs}`);
|
|
497
|
+
err.view = view;
|
|
498
|
+
return done(err);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (options.cache) {
|
|
502
|
+
this.cache[name] = view;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
view.render(options, done);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
done(/** @type {Error} */ (err));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Stops listening and emits 'close'.
|
|
515
|
+
*
|
|
516
|
+
* The callback is the first 'close' listener, so it runs before any added afterwards. Closing
|
|
517
|
+
* a server that was not listening still calls back, with an ERR_SERVER_NOT_RUNNING error, the
|
|
518
|
+
* way node does.
|
|
519
|
+
*
|
|
520
|
+
* @param {(err?: Error) => void} [callback] called once closed
|
|
521
|
+
* @returns {this} the app, for chaining
|
|
522
|
+
*/
|
|
523
|
+
close(callback) {
|
|
524
|
+
const wasListening = this.listening;
|
|
525
|
+
if (this.listenCalled && wasListening) {
|
|
526
|
+
this.uwsApp.close();
|
|
527
|
+
}
|
|
528
|
+
this.listening = false;
|
|
529
|
+
// in Express the close callback is nothing more than the first 'close' listener, and a
|
|
530
|
+
// server that was not running still gets called back, with an error
|
|
531
|
+
if (callback) {
|
|
532
|
+
this.once("close", () => {
|
|
533
|
+
if (wasListening) {
|
|
534
|
+
return callback();
|
|
535
|
+
}
|
|
536
|
+
/** @type {NodeJS.ErrnoException} */
|
|
537
|
+
const err = new Error("Server is not running.");
|
|
538
|
+
err.code = "ERR_SERVER_NOT_RUNNING";
|
|
539
|
+
callback(err);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
process.nextTick(() => this.emit("close"));
|
|
543
|
+
return this;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// An app is a function, as it is in Express, and not the Application instance whose properties it
|
|
548
|
+
// carries. Middleware that takes a whole app and calls it, vhost being the one everybody meets, was
|
|
549
|
+
// given something it could not call.
|
|
550
|
+
//
|
|
551
|
+
// This was tried once before and reverted the same day, because a callable app broke supertest:
|
|
552
|
+
// `request(app)` reads `typeof app === "function"` and wraps whatever it finds in
|
|
553
|
+
// http.createServer, and there was nothing underneath that could serve node's IncomingMessage, so
|
|
554
|
+
// every call timed out. src/node-shim.js is what closes that hole, and it is why this is safe now.
|
|
555
|
+
module.exports = function (options) {
|
|
556
|
+
return new Application(options)._asCallable();
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// the class itself, so index.js can expose its prototype as express.application does. Adding a
|
|
560
|
+
// method to that prototype adds it to every app, which is what the property is for.
|
|
561
|
+
module.exports.Application = Application;
|