@zentjs/zentjs 0.0.2
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/LICENSE +28 -0
- package/README.md +1584 -0
- package/package.json +85 -0
- package/src/core/application.mjs +1039 -0
- package/src/core/context.mjs +33 -0
- package/src/errors/error-handler.mjs +88 -0
- package/src/errors/http-error.mjs +86 -0
- package/src/hooks/lifecycle.mjs +175 -0
- package/src/http/request.mjs +166 -0
- package/src/http/response.mjs +151 -0
- package/src/index.mjs +33 -0
- package/src/middleware/pipeline.mjs +77 -0
- package/src/plugins/body-parser.mjs +165 -0
- package/src/plugins/cors.mjs +183 -0
- package/src/plugins/manager.mjs +112 -0
- package/src/plugins/request-metrics.mjs +74 -0
- package/src/router/index.mjs +198 -0
- package/src/router/node.mjs +72 -0
- package/src/router/radix-tree.mjs +313 -0
- package/src/utils/http-status.mjs +31 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application — Classe principal do framework ZentJS.
|
|
3
|
+
*
|
|
4
|
+
* Orquestra todos os componentes: Router, Lifecycle, ErrorHandler, Pipeline.
|
|
5
|
+
* Expõe a API pública para o usuário registrar rotas, middlewares,
|
|
6
|
+
* hooks e plugins, e iniciar/parar o servidor HTTP.
|
|
7
|
+
*
|
|
8
|
+
* @module core/application
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
|
|
13
|
+
import { ErrorHandler } from '../errors/error-handler.mjs';
|
|
14
|
+
import { NotFoundError } from '../errors/http-error.mjs';
|
|
15
|
+
import { HOOK_PHASES, Lifecycle } from '../hooks/lifecycle.mjs';
|
|
16
|
+
import { compose } from '../middleware/pipeline.mjs';
|
|
17
|
+
import { PluginManager } from '../plugins/manager.mjs';
|
|
18
|
+
import { Router } from '../router/index.mjs';
|
|
19
|
+
import { Context } from './context.mjs';
|
|
20
|
+
|
|
21
|
+
const HTTP_METHODS = [
|
|
22
|
+
'GET',
|
|
23
|
+
'POST',
|
|
24
|
+
'PUT',
|
|
25
|
+
'PATCH',
|
|
26
|
+
'DELETE',
|
|
27
|
+
'HEAD',
|
|
28
|
+
'OPTIONS',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const SCOPE_DECORATORS = Symbol('scopeDecorators');
|
|
32
|
+
const SCOPE_MIDDLEWARES = Symbol('scopeMiddlewares');
|
|
33
|
+
const SCOPE_HOOKS = Symbol('scopeHooks');
|
|
34
|
+
const ROUTE_HOOKS = Symbol('routeHooks');
|
|
35
|
+
const ROUTE_MIDDLEWARES = Symbol('routeMiddlewares');
|
|
36
|
+
const ROUTE_PIPELINE_CACHE = Symbol('routePipelineCache');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cria um registro de decorators com herança por escopo.
|
|
40
|
+
* @param {{ values: object } | null} parentRegistry
|
|
41
|
+
* @returns {{ values: object, has: (name: string) => boolean, define: (name: string, value: *) => void }}
|
|
42
|
+
*/
|
|
43
|
+
function createScopeDecoratorRegistry(parentRegistry = null) {
|
|
44
|
+
const values = Object.create(parentRegistry?.values || null);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
values,
|
|
48
|
+
has(name) {
|
|
49
|
+
return name in values;
|
|
50
|
+
},
|
|
51
|
+
define(name, value) {
|
|
52
|
+
if (name in values) {
|
|
53
|
+
throw new Error(`Decorator "${name}" already exists`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
values[name] = value;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clona mapa de hooks preservando funções.
|
|
63
|
+
* @param {object | null | undefined} hooks
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
function cloneHooksMap(hooks) {
|
|
67
|
+
const cloned = {};
|
|
68
|
+
|
|
69
|
+
if (!hooks) return cloned;
|
|
70
|
+
|
|
71
|
+
for (const [phase, fns] of Object.entries(hooks)) {
|
|
72
|
+
cloned[phase] = Array.isArray(fns) ? [...fns] : [fns];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return cloned;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mescla hooks preservando ordem: base -> extra.
|
|
80
|
+
* @param {object | null | undefined} baseHooks
|
|
81
|
+
* @param {object | null | undefined} extraHooks
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
function mergeHooksMap(baseHooks, extraHooks) {
|
|
85
|
+
const merged = cloneHooksMap(baseHooks);
|
|
86
|
+
|
|
87
|
+
if (!extraHooks) return merged;
|
|
88
|
+
|
|
89
|
+
for (const [phase, fns] of Object.entries(extraHooks)) {
|
|
90
|
+
const list = Array.isArray(fns) ? fns : [fns];
|
|
91
|
+
merged[phase] = [...(merged[phase] || []), ...list];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return merged;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Normaliza entrada de middlewares para array.
|
|
99
|
+
* @param {Function[] | Function | undefined} middlewares
|
|
100
|
+
* @returns {Function[]}
|
|
101
|
+
*/
|
|
102
|
+
function toMiddlewareArray(middlewares) {
|
|
103
|
+
if (!middlewares) return [];
|
|
104
|
+
return Array.isArray(middlewares) ? middlewares : [middlewares];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normaliza hooks de rota para arrays por fase.
|
|
109
|
+
* @param {object | null | undefined} hooks
|
|
110
|
+
* @returns {Record<string, Function[]>}
|
|
111
|
+
*/
|
|
112
|
+
function normalizeRouteHooks(hooks) {
|
|
113
|
+
const normalized = {};
|
|
114
|
+
|
|
115
|
+
if (!hooks) return normalized;
|
|
116
|
+
|
|
117
|
+
for (const phase of HOOK_PHASES) {
|
|
118
|
+
const phaseHooks = hooks[phase];
|
|
119
|
+
if (!phaseHooks) continue;
|
|
120
|
+
normalized[phase] = Array.isArray(phaseHooks) ? phaseHooks : [phaseHooks];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compila definição de rota para reduzir custo no hot path.
|
|
128
|
+
* @param {object} definition
|
|
129
|
+
* @returns {object}
|
|
130
|
+
*/
|
|
131
|
+
function compileRouteDefinition(definition) {
|
|
132
|
+
const routeMiddlewares = toMiddlewareArray(definition.middlewares);
|
|
133
|
+
const normalizedHooks = normalizeRouteHooks(definition.hooks);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...definition,
|
|
137
|
+
middlewares: routeMiddlewares,
|
|
138
|
+
hooks: normalizedHooks,
|
|
139
|
+
[ROUTE_MIDDLEWARES]: routeMiddlewares,
|
|
140
|
+
[ROUTE_HOOKS]: normalizedHooks,
|
|
141
|
+
[ROUTE_PIPELINE_CACHE]: {
|
|
142
|
+
version: -1,
|
|
143
|
+
pipeline: null,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Normaliza prefixo de middleware.
|
|
150
|
+
* @param {string} prefix
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
function normalizeMiddlewarePrefix(prefix) {
|
|
154
|
+
const trimmed = prefix.trim();
|
|
155
|
+
|
|
156
|
+
if (!trimmed || trimmed === '/') return '/';
|
|
157
|
+
|
|
158
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
159
|
+
return withLeadingSlash.replace(/\/+$/, '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Verifica se o path atual pertence ao prefixo configurado.
|
|
164
|
+
* @param {string} path
|
|
165
|
+
* @param {string} prefix
|
|
166
|
+
* @returns {boolean}
|
|
167
|
+
*/
|
|
168
|
+
function pathMatchesPrefix(path, prefix) {
|
|
169
|
+
if (prefix === '/') return true;
|
|
170
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Normaliza headers para lowercase simulando IncomingMessage.headers.
|
|
175
|
+
* @param {Record<string, string>} headers
|
|
176
|
+
* @returns {Record<string, string>}
|
|
177
|
+
*/
|
|
178
|
+
function normalizeInjectHeaders(headers = {}) {
|
|
179
|
+
const normalized = {};
|
|
180
|
+
|
|
181
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
182
|
+
normalized[name.toLowerCase()] = value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return normalized;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Cria uma nova instância da aplicação ZentJS.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} [opts]
|
|
192
|
+
* @param {boolean} [opts.ignoreTrailingSlash=true]
|
|
193
|
+
* @param {boolean} [opts.caseSensitive=false]
|
|
194
|
+
* @returns {Zent}
|
|
195
|
+
*/
|
|
196
|
+
export function zent(opts = {}) {
|
|
197
|
+
return new Zent(opts);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Classe principal do framework.
|
|
202
|
+
* Responsabilidade: orquestrar Router, Lifecycle, Pipeline e ErrorHandler.
|
|
203
|
+
*/
|
|
204
|
+
export class Zent {
|
|
205
|
+
/** @type {import('node:http').Server | null} */
|
|
206
|
+
#server;
|
|
207
|
+
|
|
208
|
+
/** @type {Router} */
|
|
209
|
+
#router;
|
|
210
|
+
|
|
211
|
+
/** @type {Lifecycle} */
|
|
212
|
+
#lifecycle;
|
|
213
|
+
|
|
214
|
+
/** @type {ErrorHandler} */
|
|
215
|
+
#errorHandler;
|
|
216
|
+
|
|
217
|
+
/** @type {Function[]} */
|
|
218
|
+
#middlewares;
|
|
219
|
+
|
|
220
|
+
/** @type {Record<string, *>} */
|
|
221
|
+
#decorators;
|
|
222
|
+
|
|
223
|
+
/** @type {PluginManager} */
|
|
224
|
+
#plugins;
|
|
225
|
+
|
|
226
|
+
/** @type {number} */
|
|
227
|
+
#middlewareVersion;
|
|
228
|
+
|
|
229
|
+
/** @type {Record<string, boolean>} */
|
|
230
|
+
#globalHooksActive;
|
|
231
|
+
|
|
232
|
+
/** @type {((ctx: Context) => void | Promise<void>) | null} */
|
|
233
|
+
#notFoundHandler;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {object} [opts]
|
|
237
|
+
* @param {boolean} [opts.ignoreTrailingSlash=true]
|
|
238
|
+
* @param {boolean} [opts.caseSensitive=false]
|
|
239
|
+
*/
|
|
240
|
+
constructor(opts = {}) {
|
|
241
|
+
this.#server = null;
|
|
242
|
+
this.#router = new Router({
|
|
243
|
+
ignoreTrailingSlash: opts.ignoreTrailingSlash,
|
|
244
|
+
caseSensitive: opts.caseSensitive,
|
|
245
|
+
});
|
|
246
|
+
this.#lifecycle = new Lifecycle();
|
|
247
|
+
this.#errorHandler = new ErrorHandler();
|
|
248
|
+
this.#middlewares = [];
|
|
249
|
+
this.#decorators = {};
|
|
250
|
+
this.#plugins = new PluginManager();
|
|
251
|
+
this.#notFoundHandler = null;
|
|
252
|
+
this.#middlewareVersion = 0;
|
|
253
|
+
this.#globalHooksActive = Object.fromEntries(
|
|
254
|
+
HOOK_PHASES.map((phase) => [phase, false])
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── Routing ──────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Registra uma rota completa.
|
|
262
|
+
* @param {object} definition
|
|
263
|
+
*/
|
|
264
|
+
route(definition) {
|
|
265
|
+
this.#router.route(compileRouteDefinition(definition));
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Registra rota para todos os métodos HTTP.
|
|
271
|
+
* @param {string} path
|
|
272
|
+
* @param {Function} handler
|
|
273
|
+
* @param {object} [opts]
|
|
274
|
+
*/
|
|
275
|
+
all(path, handler, opts) {
|
|
276
|
+
this.#router.all(path, handler, opts);
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Agrupa rotas sob um prefixo.
|
|
282
|
+
* @param {string} prefix
|
|
283
|
+
* @param {...any} args
|
|
284
|
+
*/
|
|
285
|
+
group(prefix, ...args) {
|
|
286
|
+
this.#router.group(prefix, ...args);
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Middleware ───────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Registra middleware global ou com prefixo.
|
|
294
|
+
* @param {Function|string} arg1 - middleware ou prefixo
|
|
295
|
+
* @param {Function} [arg2] - middleware quando usar prefixo
|
|
296
|
+
* @returns {this}
|
|
297
|
+
*/
|
|
298
|
+
use(arg1, arg2) {
|
|
299
|
+
if (typeof arg1 === 'function' && arg2 === undefined) {
|
|
300
|
+
this.#middlewares.push(arg1);
|
|
301
|
+
this.#middlewareVersion++;
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (typeof arg1 === 'string' && typeof arg2 === 'function') {
|
|
306
|
+
const prefix = normalizeMiddlewarePrefix(arg1);
|
|
307
|
+
|
|
308
|
+
this.#middlewares.push(async (ctx, next) => {
|
|
309
|
+
if (!pathMatchesPrefix(ctx.req.path, prefix)) {
|
|
310
|
+
return next();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return arg2(ctx, next);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
this.#middlewareVersion++;
|
|
317
|
+
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (arg2 === undefined) {
|
|
322
|
+
throw new TypeError(`Middleware must be a function, got ${typeof arg1}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
throw new TypeError(
|
|
326
|
+
'Invalid use() signature. Expected use(middleware) or use(prefix, middleware)'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Lifecycle Hooks ──────────────────────────────────
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Registra um hook de lifecycle.
|
|
334
|
+
* @param {string} phase
|
|
335
|
+
* @param {Function} fn
|
|
336
|
+
* @returns {this}
|
|
337
|
+
*/
|
|
338
|
+
addHook(phase, fn) {
|
|
339
|
+
this.#lifecycle.addHook(phase, fn);
|
|
340
|
+
this.#globalHooksActive[phase] = true;
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Error Handling ───────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Define um error handler customizado.
|
|
348
|
+
* @param {Function} fn - (error, ctx) => {}
|
|
349
|
+
* @returns {this}
|
|
350
|
+
*/
|
|
351
|
+
setErrorHandler(fn) {
|
|
352
|
+
this.#errorHandler.setErrorHandler(fn);
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Define um handler customizado para rotas não encontradas (404).
|
|
358
|
+
* @param {(ctx: Context) => void | Promise<void>} fn
|
|
359
|
+
* @returns {this}
|
|
360
|
+
*/
|
|
361
|
+
setNotFoundHandler(fn) {
|
|
362
|
+
if (typeof fn !== 'function') {
|
|
363
|
+
throw new TypeError(
|
|
364
|
+
`Not found handler must be a function, got ${typeof fn}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.#notFoundHandler = fn;
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── Decorators ───────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Adiciona uma propriedade/método à instância da aplicação.
|
|
376
|
+
* Acessível via ctx.app.nome nos handlers.
|
|
377
|
+
* @param {string} name
|
|
378
|
+
* @param {*} value
|
|
379
|
+
* @returns {this}
|
|
380
|
+
*/
|
|
381
|
+
decorate(name, value) {
|
|
382
|
+
if (name in this) {
|
|
383
|
+
throw new Error(`Decorator "${name}" already exists`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this.#decorators[name] = value;
|
|
387
|
+
this[name] = value;
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Verifica se um decorator existe.
|
|
393
|
+
* @param {string} name
|
|
394
|
+
* @returns {boolean}
|
|
395
|
+
*/
|
|
396
|
+
hasDecorator(name) {
|
|
397
|
+
return name in this.#decorators;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── Plugins ──────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Registra um plugin para ser carregado antes do listen/inject.
|
|
404
|
+
* Cada plugin recebe uma instância encapsulada do app.
|
|
405
|
+
*
|
|
406
|
+
* @param {Function} fn - async (app, opts) => {}
|
|
407
|
+
* @param {object} [opts={}] - Opções do plugin (ex: { prefix: '/api' })
|
|
408
|
+
* @returns {this}
|
|
409
|
+
*/
|
|
410
|
+
register(fn, opts = {}) {
|
|
411
|
+
this.#plugins.register(fn, opts);
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Carrega todos os plugins registrados se ainda não foram carregados.
|
|
417
|
+
* Cria escopos encapsulados: rotas do plugin são prefixadas,
|
|
418
|
+
* hooks e decorators são isolados no escopo, middlewares são herdados.
|
|
419
|
+
*
|
|
420
|
+
* @returns {Promise<void>}
|
|
421
|
+
*/
|
|
422
|
+
async #loadPlugins() {
|
|
423
|
+
if (this.#plugins.loaded) return;
|
|
424
|
+
|
|
425
|
+
await this.#plugins.load((opts) => this.#createScope(opts));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Cria um escopo encapsulado para um plugin.
|
|
430
|
+
* O escopo herda middlewares e hooks do pai mas adiciona
|
|
431
|
+
* rotas e decorators de forma isolada.
|
|
432
|
+
*
|
|
433
|
+
* @param {object} opts - Opções do plugin
|
|
434
|
+
* @returns {object} Escopo encapsulado com a mesma API do Zent
|
|
435
|
+
*/
|
|
436
|
+
#createScope(opts) {
|
|
437
|
+
const prefix = opts.prefix || '';
|
|
438
|
+
const parent = this;
|
|
439
|
+
const decoratorRegistry = createScopeDecoratorRegistry(
|
|
440
|
+
opts[SCOPE_DECORATORS] || null
|
|
441
|
+
);
|
|
442
|
+
const scopeMiddlewares = [...(opts[SCOPE_MIDDLEWARES] || [])];
|
|
443
|
+
const scopeHooks = cloneHooksMap(opts[SCOPE_HOOKS]);
|
|
444
|
+
|
|
445
|
+
const scope = {};
|
|
446
|
+
|
|
447
|
+
const scopeDecorate = (name, value) => {
|
|
448
|
+
if (name in scope) {
|
|
449
|
+
throw new Error(`Decorator "${name}" already exists`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
decoratorRegistry.define(name, value);
|
|
453
|
+
scope[name] = value;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const scopeHasDecorator = (name) => decoratorRegistry.has(name);
|
|
457
|
+
|
|
458
|
+
const scopeUse = (arg1, arg2) => {
|
|
459
|
+
if (typeof arg1 === 'function' && arg2 === undefined) {
|
|
460
|
+
scopeMiddlewares.push(arg1);
|
|
461
|
+
return scope;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof arg1 === 'string' && typeof arg2 === 'function') {
|
|
465
|
+
const scopedPrefix = `${prefix}${arg1.startsWith('/') ? '' : '/'}${arg1}`;
|
|
466
|
+
const localPrefix = normalizeMiddlewarePrefix(scopedPrefix);
|
|
467
|
+
|
|
468
|
+
scopeMiddlewares.push(async (ctx, next) => {
|
|
469
|
+
if (!pathMatchesPrefix(ctx.req.path, localPrefix)) {
|
|
470
|
+
return next();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return arg2(ctx, next);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return scope;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (arg2 === undefined) {
|
|
480
|
+
throw new TypeError(
|
|
481
|
+
`Middleware must be a function, got ${typeof arg1}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
throw new TypeError(
|
|
486
|
+
'Invalid use() signature. Expected use(middleware) or use(prefix, middleware)'
|
|
487
|
+
);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const scopeAddHook = (phase, fn) => {
|
|
491
|
+
if (!HOOK_PHASES.includes(phase)) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Invalid hook phase: "${phase}". Valid phases: ${HOOK_PHASES.join(', ')}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (typeof fn !== 'function') {
|
|
498
|
+
throw new TypeError(`Hook must be a function, got ${typeof fn}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const existing = scopeHooks[phase] || [];
|
|
502
|
+
scopeHooks[phase] = [...existing, fn];
|
|
503
|
+
return scope;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const withScopeRouteOpts = (routeOpts = {}) => {
|
|
507
|
+
const routeMiddlewares = toMiddlewareArray(routeOpts.middlewares);
|
|
508
|
+
const mergedMiddlewares = [...scopeMiddlewares, ...routeMiddlewares];
|
|
509
|
+
const mergedHooks = mergeHooksMap(scopeHooks, routeOpts.hooks || {});
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
...routeOpts,
|
|
513
|
+
middlewares: mergedMiddlewares,
|
|
514
|
+
hooks: mergedHooks,
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const registerMethod =
|
|
519
|
+
(method) =>
|
|
520
|
+
(path, handler, routeOpts = {}) => {
|
|
521
|
+
return parent[method](
|
|
522
|
+
prefix + path,
|
|
523
|
+
handler,
|
|
524
|
+
withScopeRouteOpts(routeOpts)
|
|
525
|
+
);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const scopeRoute = (def) => {
|
|
529
|
+
const routeOpts = withScopeRouteOpts({
|
|
530
|
+
middlewares: def.middlewares,
|
|
531
|
+
hooks: def.hooks,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
return parent.route({
|
|
535
|
+
...def,
|
|
536
|
+
path: prefix + def.path,
|
|
537
|
+
middlewares: routeOpts.middlewares,
|
|
538
|
+
hooks: routeOpts.hooks,
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const scopeAll = (path, handler, routeOpts = {}) => {
|
|
543
|
+
for (const method of HTTP_METHODS) {
|
|
544
|
+
scopeRoute({ method, path, handler, ...routeOpts });
|
|
545
|
+
}
|
|
546
|
+
return scope;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const scopeGroup = (groupPrefix, ...args) => {
|
|
550
|
+
const groupOpts = typeof args[0] === 'function' ? {} : args.shift() || {};
|
|
551
|
+
const callback = args[0];
|
|
552
|
+
|
|
553
|
+
const mergedGroupOpts = {
|
|
554
|
+
...groupOpts,
|
|
555
|
+
middlewares: [
|
|
556
|
+
...scopeMiddlewares,
|
|
557
|
+
...toMiddlewareArray(groupOpts.middlewares),
|
|
558
|
+
],
|
|
559
|
+
hooks: mergeHooksMap(scopeHooks, groupOpts.hooks || {}),
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
parent.group(prefix + groupPrefix, mergedGroupOpts, callback);
|
|
563
|
+
return scope;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return Object.assign(scope, {
|
|
567
|
+
get: registerMethod('get'),
|
|
568
|
+
post: registerMethod('post'),
|
|
569
|
+
put: registerMethod('put'),
|
|
570
|
+
patch: registerMethod('patch'),
|
|
571
|
+
delete: registerMethod('delete'),
|
|
572
|
+
head: registerMethod('head'),
|
|
573
|
+
options: registerMethod('options'),
|
|
574
|
+
all: scopeAll,
|
|
575
|
+
route: scopeRoute,
|
|
576
|
+
group: scopeGroup,
|
|
577
|
+
use: scopeUse,
|
|
578
|
+
addHook: scopeAddHook,
|
|
579
|
+
setErrorHandler: (fn) => parent.setErrorHandler(fn),
|
|
580
|
+
setNotFoundHandler: (fn) => parent.setNotFoundHandler(fn),
|
|
581
|
+
decorate: scopeDecorate,
|
|
582
|
+
hasDecorator: scopeHasDecorator,
|
|
583
|
+
register: (fn, pluginOpts) => {
|
|
584
|
+
const nextOpts = {
|
|
585
|
+
...(pluginOpts || {}),
|
|
586
|
+
prefix: prefix + (pluginOpts?.prefix || ''),
|
|
587
|
+
[SCOPE_DECORATORS]: decoratorRegistry,
|
|
588
|
+
[SCOPE_MIDDLEWARES]: [...scopeMiddlewares],
|
|
589
|
+
[SCOPE_HOOKS]: cloneHooksMap(scopeHooks),
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
parent.#plugins.register((scopedApp, resolvedOpts) => {
|
|
593
|
+
return fn(scopedApp, resolvedOpts);
|
|
594
|
+
}, nextOpts);
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ─── Server ───────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Inicia o servidor HTTP.
|
|
603
|
+
* @param {object} [opts]
|
|
604
|
+
* @param {number} [opts.port=3000]
|
|
605
|
+
* @param {string} [opts.host='0.0.0.0']
|
|
606
|
+
* @param {Function} [callback] - (err, address) => {}
|
|
607
|
+
* @returns {Promise<string>} Endereço do servidor
|
|
608
|
+
*/
|
|
609
|
+
async listen(opts = {}, callback) {
|
|
610
|
+
await this.#loadPlugins();
|
|
611
|
+
|
|
612
|
+
const port = opts.port ?? 3000;
|
|
613
|
+
const host = opts.host ?? '0.0.0.0';
|
|
614
|
+
|
|
615
|
+
this.#server = createServer((req, res) => {
|
|
616
|
+
this.#handleRequest(req, res);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
return new Promise((resolve, reject) => {
|
|
620
|
+
this.#server.listen(port, host, () => {
|
|
621
|
+
const boundPort = this.#server.address().port;
|
|
622
|
+
const address = `http://${host}:${boundPort}`;
|
|
623
|
+
|
|
624
|
+
if (callback) {
|
|
625
|
+
callback(null, address);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
resolve(address);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
this.#server.on('error', (err) => {
|
|
632
|
+
if (callback) {
|
|
633
|
+
callback(err);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
reject(err);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Encerra o servidor HTTP.
|
|
643
|
+
* @returns {Promise<void>}
|
|
644
|
+
*/
|
|
645
|
+
async close() {
|
|
646
|
+
if (!this.#server) return;
|
|
647
|
+
|
|
648
|
+
return new Promise((resolve, reject) => {
|
|
649
|
+
this.#server.close((err) => {
|
|
650
|
+
this.#server = null;
|
|
651
|
+
|
|
652
|
+
if (err) {
|
|
653
|
+
reject(err);
|
|
654
|
+
} else {
|
|
655
|
+
resolve();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ─── Inject (test helper) ────────────────────────────
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Simula uma requisição HTTP sem abrir uma porta de rede.
|
|
665
|
+
* Ideal para testes unitários/integração.
|
|
666
|
+
*
|
|
667
|
+
* @param {object} opts
|
|
668
|
+
* @param {string} opts.method - HTTP method
|
|
669
|
+
* @param {string} opts.url - Request path
|
|
670
|
+
* @param {Record<string, string>} [opts.headers] - Headers
|
|
671
|
+
* @param {string | object} [opts.body] - Body (string ou objeto → JSON)
|
|
672
|
+
* @returns {Promise<{ statusCode: number, headers: object, body: string, json: Function }>}
|
|
673
|
+
*/
|
|
674
|
+
async inject(opts) {
|
|
675
|
+
await this.#loadPlugins();
|
|
676
|
+
|
|
677
|
+
const { method = 'GET', url = '/', headers = {}, body } = opts;
|
|
678
|
+
const normalizedHeaders = normalizeInjectHeaders(headers);
|
|
679
|
+
|
|
680
|
+
// Prepara body serializado
|
|
681
|
+
let bodyStr;
|
|
682
|
+
if (body !== undefined && body !== null) {
|
|
683
|
+
if (typeof body === 'object') {
|
|
684
|
+
bodyStr = JSON.stringify(body);
|
|
685
|
+
normalizedHeaders['content-type'] =
|
|
686
|
+
normalizedHeaders['content-type'] || 'application/json';
|
|
687
|
+
} else {
|
|
688
|
+
bodyStr = String(body);
|
|
689
|
+
}
|
|
690
|
+
normalizedHeaders['content-length'] = String(Buffer.byteLength(bodyStr));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Mock do IncomingMessage
|
|
694
|
+
const rawReq = {
|
|
695
|
+
method: method.toUpperCase(),
|
|
696
|
+
url,
|
|
697
|
+
headers: { host: 'localhost', ...normalizedHeaders },
|
|
698
|
+
socket: { remoteAddress: '127.0.0.1', encrypted: false },
|
|
699
|
+
body: bodyStr ?? null,
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// Mock do ServerResponse — acumula dados escritos
|
|
703
|
+
const chunks = [];
|
|
704
|
+
let headersWritten = {};
|
|
705
|
+
let statusCode = 200;
|
|
706
|
+
|
|
707
|
+
const rawRes = {
|
|
708
|
+
writableEnded: false,
|
|
709
|
+
|
|
710
|
+
setHeader(name, value) {
|
|
711
|
+
headersWritten[name.toLowerCase()] = value;
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
getHeader(name) {
|
|
715
|
+
return headersWritten[name.toLowerCase()];
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
writeHead(code) {
|
|
719
|
+
statusCode = code;
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
end(chunk) {
|
|
723
|
+
if (chunk !== undefined) {
|
|
724
|
+
chunks.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
725
|
+
}
|
|
726
|
+
this.writableEnded = true;
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Executa o request dispatch
|
|
731
|
+
await this.#handleRequest(rawReq, rawRes);
|
|
732
|
+
|
|
733
|
+
const responseBody = chunks.join('');
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
statusCode,
|
|
737
|
+
headers: headersWritten,
|
|
738
|
+
body: responseBody,
|
|
739
|
+
json() {
|
|
740
|
+
return JSON.parse(responseBody);
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ─── Request Dispatch (internal) ─────────────────────
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Processa uma requisição HTTP completa.
|
|
749
|
+
* Orquestra: Context → Hooks → Router → Middleware → Handler → Response.
|
|
750
|
+
*
|
|
751
|
+
* @param {import('node:http').IncomingMessage} rawReq
|
|
752
|
+
* @param {import('node:http').ServerResponse} rawRes
|
|
753
|
+
*/
|
|
754
|
+
async #handleRequest(rawReq, rawRes) {
|
|
755
|
+
const ctx = new Context(rawReq, rawRes, this);
|
|
756
|
+
let route = null;
|
|
757
|
+
let routeHooks = null;
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
let handlerResult;
|
|
761
|
+
|
|
762
|
+
// 1. onRequest hooks
|
|
763
|
+
if (this.#globalHooksActive.onRequest) {
|
|
764
|
+
await this.#lifecycle.run('onRequest', ctx);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 2. Router lookup
|
|
768
|
+
const matchedRoute = await this.#findRoute(ctx);
|
|
769
|
+
|
|
770
|
+
if (!matchedRoute) {
|
|
771
|
+
if (this.#globalHooksActive.onResponse) {
|
|
772
|
+
await this.#lifecycle.run('onResponse', ctx);
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const { route: resolvedRoute, params } = matchedRoute;
|
|
778
|
+
route = resolvedRoute;
|
|
779
|
+
this.#ensureCompiledRoute(route);
|
|
780
|
+
routeHooks = route[ROUTE_HOOKS];
|
|
781
|
+
|
|
782
|
+
// 3. Set params no request
|
|
783
|
+
ctx.req.params = params;
|
|
784
|
+
|
|
785
|
+
// 3.1 route-level onRequest hooks
|
|
786
|
+
await this.#runHooksList(routeHooks.onRequest, ctx);
|
|
787
|
+
|
|
788
|
+
// 4. preParsing hooks
|
|
789
|
+
if (this.#globalHooksActive.preParsing) {
|
|
790
|
+
await this.#lifecycle.run('preParsing', ctx);
|
|
791
|
+
}
|
|
792
|
+
await this.#runHooksList(routeHooks.preParsing, ctx);
|
|
793
|
+
|
|
794
|
+
// 5. preValidation hooks
|
|
795
|
+
if (this.#globalHooksActive.preValidation) {
|
|
796
|
+
await this.#lifecycle.run('preValidation', ctx);
|
|
797
|
+
}
|
|
798
|
+
await this.#runHooksList(routeHooks.preValidation, ctx);
|
|
799
|
+
|
|
800
|
+
// 6. Montar pipeline: global middlewares + route middlewares + handler
|
|
801
|
+
// 7. preHandler hooks (executados dentro do pipeline, antes do handler)
|
|
802
|
+
const handler = async (ctx) => {
|
|
803
|
+
if (this.#globalHooksActive.preHandler) {
|
|
804
|
+
await this.#lifecycle.run('preHandler', ctx);
|
|
805
|
+
}
|
|
806
|
+
await this.#runHooksList(routeHooks.preHandler, ctx);
|
|
807
|
+
|
|
808
|
+
handlerResult = await route.handler(ctx);
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// 8. Execute pipeline
|
|
812
|
+
const pipeline = this.#getRoutePipeline(route);
|
|
813
|
+
await pipeline(ctx, handler);
|
|
814
|
+
|
|
815
|
+
// 8.1 onSend + envio automático para payload retornado pelo handler
|
|
816
|
+
if (!ctx.res.sent && handlerResult !== undefined) {
|
|
817
|
+
let payload = handlerResult;
|
|
818
|
+
|
|
819
|
+
if (this.#globalHooksActive.onSend) {
|
|
820
|
+
payload = await this.#lifecycle.run('onSend', ctx, payload);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
payload = await this.#runOnSendHooksList(
|
|
824
|
+
routeHooks.onSend,
|
|
825
|
+
ctx,
|
|
826
|
+
payload
|
|
827
|
+
);
|
|
828
|
+
this.#sendPayload(ctx, payload);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// 9. onResponse hooks (após a resposta ser preparada/enviada)
|
|
832
|
+
if (this.#globalHooksActive.onResponse) {
|
|
833
|
+
await this.#lifecycle.run('onResponse', ctx);
|
|
834
|
+
}
|
|
835
|
+
await this.#runHooksList(routeHooks.onResponse, ctx);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
// Executa onError hooks
|
|
838
|
+
if (this.#globalHooksActive.onError) {
|
|
839
|
+
try {
|
|
840
|
+
await this.#lifecycle.run('onError', ctx, error);
|
|
841
|
+
} catch {
|
|
842
|
+
// Se onError hook falhar, continua para o error handler
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (route) {
|
|
847
|
+
try {
|
|
848
|
+
await this.#runRouteHooks(route, 'onError', ctx, error);
|
|
849
|
+
} catch {
|
|
850
|
+
// Se onError da rota falhar, continua para o error handler
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
await this.#errorHandler.handle(error, ctx);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Executa hooks de rota para uma fase (exceto onSend).
|
|
860
|
+
* @param {object | null} route
|
|
861
|
+
* @param {string} phase
|
|
862
|
+
* @param {Context} ctx
|
|
863
|
+
* @param {...*} args
|
|
864
|
+
* @returns {Promise<void>}
|
|
865
|
+
*/
|
|
866
|
+
async #runRouteHooks(route, phase, ctx, ...args) {
|
|
867
|
+
if (route) {
|
|
868
|
+
this.#ensureCompiledRoute(route);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const hooks = route?.[ROUTE_HOOKS]?.[phase];
|
|
872
|
+
if (!hooks || hooks.length === 0) return;
|
|
873
|
+
|
|
874
|
+
await this.#runHooksList(hooks, ctx, ...args);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Executa uma lista de hooks sequencialmente.
|
|
879
|
+
* @param {Function[] | undefined} hooks
|
|
880
|
+
* @param {Context} ctx
|
|
881
|
+
* @param {...*} args
|
|
882
|
+
*/
|
|
883
|
+
async #runHooksList(hooks, ctx, ...args) {
|
|
884
|
+
if (!hooks || hooks.length === 0) return;
|
|
885
|
+
|
|
886
|
+
for (const hook of hooks) {
|
|
887
|
+
await hook(ctx, ...args);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Executa hooks onSend encadeando payload.
|
|
893
|
+
* @param {Function[] | undefined} hooks
|
|
894
|
+
* @param {Context} ctx
|
|
895
|
+
* @param {*} payload
|
|
896
|
+
* @returns {Promise<*>}
|
|
897
|
+
*/
|
|
898
|
+
async #runOnSendHooksList(hooks, ctx, payload) {
|
|
899
|
+
if (!hooks || hooks.length === 0) return payload;
|
|
900
|
+
|
|
901
|
+
let current = payload;
|
|
902
|
+
|
|
903
|
+
for (const hook of hooks) {
|
|
904
|
+
const result = await hook(ctx, current);
|
|
905
|
+
if (result !== undefined) {
|
|
906
|
+
current = result;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return current;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Retorna pipeline compilado por rota com cache por versão de middlewares globais.
|
|
915
|
+
* @param {object} route
|
|
916
|
+
* @returns {(ctx: object, finalHandler?: Function) => Promise<void>}
|
|
917
|
+
*/
|
|
918
|
+
#getRoutePipeline(route) {
|
|
919
|
+
this.#ensureCompiledRoute(route);
|
|
920
|
+
|
|
921
|
+
const cache = route[ROUTE_PIPELINE_CACHE];
|
|
922
|
+
|
|
923
|
+
if (cache && cache.version === this.#middlewareVersion && cache.pipeline) {
|
|
924
|
+
return cache.pipeline;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const routeMiddlewares = route[ROUTE_MIDDLEWARES] || [];
|
|
928
|
+
const allMiddlewares = [...this.#middlewares, ...routeMiddlewares];
|
|
929
|
+
const pipeline = compose(allMiddlewares);
|
|
930
|
+
|
|
931
|
+
route[ROUTE_PIPELINE_CACHE] = {
|
|
932
|
+
version: this.#middlewareVersion,
|
|
933
|
+
pipeline,
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
return pipeline;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Garante metadados compilados para rotas não compiladas previamente.
|
|
941
|
+
* @param {object} route
|
|
942
|
+
*/
|
|
943
|
+
#ensureCompiledRoute(route) {
|
|
944
|
+
if (
|
|
945
|
+
route[ROUTE_HOOKS] &&
|
|
946
|
+
route[ROUTE_MIDDLEWARES] &&
|
|
947
|
+
route[ROUTE_PIPELINE_CACHE]
|
|
948
|
+
) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const routeMiddlewares = toMiddlewareArray(route.middlewares);
|
|
953
|
+
const normalizedHooks = normalizeRouteHooks(route.hooks);
|
|
954
|
+
|
|
955
|
+
route.middlewares = routeMiddlewares;
|
|
956
|
+
route.hooks = normalizedHooks;
|
|
957
|
+
route[ROUTE_MIDDLEWARES] = routeMiddlewares;
|
|
958
|
+
route[ROUTE_HOOKS] = normalizedHooks;
|
|
959
|
+
route[ROUTE_PIPELINE_CACHE] = {
|
|
960
|
+
version: -1,
|
|
961
|
+
pipeline: null,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Busca rota e aplica notFound handler customizado quando definido.
|
|
967
|
+
* @param {Context} ctx
|
|
968
|
+
* @returns {{ route: object, params: Record<string, string> }}
|
|
969
|
+
*/
|
|
970
|
+
async #findRoute(ctx) {
|
|
971
|
+
try {
|
|
972
|
+
return this.#router.find(ctx.req.method, ctx.req.path);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
if (
|
|
975
|
+
error instanceof NotFoundError &&
|
|
976
|
+
this.#notFoundHandler &&
|
|
977
|
+
!ctx.res.sent
|
|
978
|
+
) {
|
|
979
|
+
await this.#handleNotFound(ctx);
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
throw error;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Executa handler customizado de 404.
|
|
989
|
+
* @param {Context} ctx
|
|
990
|
+
* @returns {Promise<void>}
|
|
991
|
+
*/
|
|
992
|
+
async #handleNotFound(ctx) {
|
|
993
|
+
await this.#notFoundHandler(ctx);
|
|
994
|
+
|
|
995
|
+
if (!ctx.res.sent) {
|
|
996
|
+
ctx.res.status(404).json(new NotFoundError().toJSON());
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Envia payload retornado por handler quando a resposta ainda não foi enviada.
|
|
1002
|
+
* @param {Context} ctx
|
|
1003
|
+
* @param {*} payload
|
|
1004
|
+
*/
|
|
1005
|
+
#sendPayload(ctx, payload) {
|
|
1006
|
+
if (ctx.res.sent || payload === undefined) return;
|
|
1007
|
+
|
|
1008
|
+
if (payload === null) {
|
|
1009
|
+
ctx.res.send('null');
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (Buffer.isBuffer(payload) || typeof payload === 'string') {
|
|
1014
|
+
ctx.res.send(payload);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (typeof payload === 'object') {
|
|
1019
|
+
ctx.res.json(payload);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
ctx.res.send(String(payload));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ─── Convenience Route Methods ──────────────────────────
|
|
1028
|
+
|
|
1029
|
+
for (const method of HTTP_METHODS) {
|
|
1030
|
+
/**
|
|
1031
|
+
* @param {string} path
|
|
1032
|
+
* @param {Function} handler
|
|
1033
|
+
* @param {object} [opts]
|
|
1034
|
+
*/
|
|
1035
|
+
Zent.prototype[method.toLowerCase()] = function (path, handler, opts) {
|
|
1036
|
+
this.route({ method, path, handler, ...(opts || {}) });
|
|
1037
|
+
return this;
|
|
1038
|
+
};
|
|
1039
|
+
}
|