@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.
@@ -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
+ }