express-fastify-runtime 0.1.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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/changelog/README.md +5 -0
  4. package/changelog/log-2025-02-07.md +10 -0
  5. package/changelog/log-2026-06-25.md +151 -0
  6. package/dist/app/ExpressLikeApp.d.ts +9 -0
  7. package/dist/app/ExpressLikeApp.d.ts.map +1 -0
  8. package/dist/app/ExpressLikeApp.js +64 -0
  9. package/dist/app/ExpressLikeApp.js.map +1 -0
  10. package/dist/app/RouteStore.d.ts +17 -0
  11. package/dist/app/RouteStore.d.ts.map +1 -0
  12. package/dist/app/RouteStore.js +43 -0
  13. package/dist/app/RouteStore.js.map +1 -0
  14. package/dist/app/classify.d.ts +8 -0
  15. package/dist/app/classify.d.ts.map +1 -0
  16. package/dist/app/classify.js +25 -0
  17. package/dist/app/classify.js.map +1 -0
  18. package/dist/app/compile.d.ts +25 -0
  19. package/dist/app/compile.d.ts.map +1 -0
  20. package/dist/app/compile.js +195 -0
  21. package/dist/app/compile.js.map +1 -0
  22. package/dist/app/flattenRouter.d.ts +51 -0
  23. package/dist/app/flattenRouter.d.ts.map +1 -0
  24. package/dist/app/flattenRouter.js +126 -0
  25. package/dist/app/flattenRouter.js.map +1 -0
  26. package/dist/app/introspectExpress.d.ts +29 -0
  27. package/dist/app/introspectExpress.d.ts.map +1 -0
  28. package/dist/app/introspectExpress.js +60 -0
  29. package/dist/app/introspectExpress.js.map +1 -0
  30. package/dist/examples/auth.d.ts +6 -0
  31. package/dist/examples/auth.d.ts.map +1 -0
  32. package/dist/examples/auth.js +26 -0
  33. package/dist/examples/auth.js.map +1 -0
  34. package/dist/examples/logging.d.ts +6 -0
  35. package/dist/examples/logging.d.ts.map +1 -0
  36. package/dist/examples/logging.js +25 -0
  37. package/dist/examples/logging.js.map +1 -0
  38. package/dist/examples/uploads.d.ts +6 -0
  39. package/dist/examples/uploads.d.ts.map +1 -0
  40. package/dist/examples/uploads.js +21 -0
  41. package/dist/examples/uploads.js.map +1 -0
  42. package/dist/express/engine.d.ts +6 -0
  43. package/dist/express/engine.d.ts.map +1 -0
  44. package/dist/express/engine.js +14 -0
  45. package/dist/express/engine.js.map +1 -0
  46. package/dist/express/middleware.d.ts +15 -0
  47. package/dist/express/middleware.d.ts.map +1 -0
  48. package/dist/express/middleware.js +32 -0
  49. package/dist/express/middleware.js.map +1 -0
  50. package/dist/express/mount.d.ts +35 -0
  51. package/dist/express/mount.d.ts.map +1 -0
  52. package/dist/express/mount.js +78 -0
  53. package/dist/express/mount.js.map +1 -0
  54. package/dist/fastify/adapters/middleware.d.ts +8 -0
  55. package/dist/fastify/adapters/middleware.d.ts.map +1 -0
  56. package/dist/fastify/adapters/middleware.js +29 -0
  57. package/dist/fastify/adapters/middleware.js.map +1 -0
  58. package/dist/fastify/adapters/request.d.ts +19 -0
  59. package/dist/fastify/adapters/request.d.ts.map +1 -0
  60. package/dist/fastify/adapters/request.js +258 -0
  61. package/dist/fastify/adapters/request.js.map +1 -0
  62. package/dist/fastify/adapters/response.d.ts +19 -0
  63. package/dist/fastify/adapters/response.d.ts.map +1 -0
  64. package/dist/fastify/adapters/response.js +667 -0
  65. package/dist/fastify/adapters/response.js.map +1 -0
  66. package/dist/fastify/register.d.ts +12 -0
  67. package/dist/fastify/register.d.ts.map +1 -0
  68. package/dist/fastify/register.js +15 -0
  69. package/dist/fastify/register.js.map +1 -0
  70. package/dist/index.d.ts +13 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +18 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/runtime/decorators.d.ts +7 -0
  75. package/dist/runtime/decorators.d.ts.map +1 -0
  76. package/dist/runtime/decorators.js +17 -0
  77. package/dist/runtime/decorators.js.map +1 -0
  78. package/dist/runtime/errorHandler.d.ts +13 -0
  79. package/dist/runtime/errorHandler.d.ts.map +1 -0
  80. package/dist/runtime/errorHandler.js +95 -0
  81. package/dist/runtime/errorHandler.js.map +1 -0
  82. package/dist/runtime/expressLane.d.ts +40 -0
  83. package/dist/runtime/expressLane.d.ts.map +1 -0
  84. package/dist/runtime/expressLane.js +71 -0
  85. package/dist/runtime/expressLane.js.map +1 -0
  86. package/dist/runtime/fast.d.ts +43 -0
  87. package/dist/runtime/fast.d.ts.map +1 -0
  88. package/dist/runtime/fast.js +150 -0
  89. package/dist/runtime/fast.js.map +1 -0
  90. package/dist/runtime/lifecycle.d.ts +10 -0
  91. package/dist/runtime/lifecycle.d.ts.map +1 -0
  92. package/dist/runtime/lifecycle.js +152 -0
  93. package/dist/runtime/lifecycle.js.map +1 -0
  94. package/dist/runtime/populateExpress.d.ts +7 -0
  95. package/dist/runtime/populateExpress.d.ts.map +1 -0
  96. package/dist/runtime/populateExpress.js +27 -0
  97. package/dist/runtime/populateExpress.js.map +1 -0
  98. package/dist/types/express.d.ts +97 -0
  99. package/dist/types/express.d.ts.map +1 -0
  100. package/dist/types/express.js +12 -0
  101. package/dist/types/express.js.map +1 -0
  102. package/dist/types/internal.d.ts +60 -0
  103. package/dist/types/internal.d.ts.map +1 -0
  104. package/dist/types/internal.js +7 -0
  105. package/dist/types/internal.js.map +1 -0
  106. package/dist/utils/assert.d.ts +6 -0
  107. package/dist/utils/assert.d.ts.map +1 -0
  108. package/dist/utils/assert.js +17 -0
  109. package/dist/utils/assert.js.map +1 -0
  110. package/dist/utils/detect.d.ts +14 -0
  111. package/dist/utils/detect.d.ts.map +1 -0
  112. package/dist/utils/detect.js +64 -0
  113. package/dist/utils/detect.js.map +1 -0
  114. package/dist/utils/maxListeners.d.ts +28 -0
  115. package/dist/utils/maxListeners.d.ts.map +1 -0
  116. package/dist/utils/maxListeners.js +50 -0
  117. package/dist/utils/maxListeners.js.map +1 -0
  118. package/dist/utils/patchRouterLayer.d.ts +12 -0
  119. package/dist/utils/patchRouterLayer.d.ts.map +1 -0
  120. package/dist/utils/patchRouterLayer.js +96 -0
  121. package/dist/utils/patchRouterLayer.js.map +1 -0
  122. package/dist/utils/path.d.ts +6 -0
  123. package/dist/utils/path.d.ts.map +1 -0
  124. package/dist/utils/path.js +23 -0
  125. package/dist/utils/path.js.map +1 -0
  126. package/dist/utils/runtimeLogger.d.ts +15 -0
  127. package/dist/utils/runtimeLogger.d.ts.map +1 -0
  128. package/dist/utils/runtimeLogger.js +28 -0
  129. package/dist/utils/runtimeLogger.js.map +1 -0
  130. package/dist/utils/unwrap.d.ts +12 -0
  131. package/dist/utils/unwrap.d.ts.map +1 -0
  132. package/dist/utils/unwrap.js +24 -0
  133. package/dist/utils/unwrap.js.map +1 -0
  134. package/package.json +94 -0
@@ -0,0 +1,667 @@
1
+ "use strict";
2
+ /**
3
+ * FastifyReply → Express-like res.
4
+ * Full Express 5.x response API so existing apps work unchanged.
5
+ * Aligns with Express 5.2.1 (see docs/EXPRESS_REFERENCE.md); uses encodeurl for location().
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.adaptResponse = adaptResponse;
12
+ exports.createResponseAdapter = createResponseAdapter;
13
+ const node_http_1 = require("node:http");
14
+ const encodeurl_1 = __importDefault(require("encodeurl"));
15
+ const STATUS_CODES = node_http_1.STATUS_CODES;
16
+ function normalizeHeaderValue(value) {
17
+ if (Array.isArray(value))
18
+ return value.join(', ');
19
+ return String(value);
20
+ }
21
+ /**
22
+ * Record res._startAt (high-res time) the first time we send, so morgan's :response-time
23
+ * token has the value it normally gets from on-headers (which patches the real writeHead).
24
+ * Set at send time ≈ header-write time; morgan computes response-time = res._startAt - req._startAt.
25
+ */
26
+ function recordStartAt(self) {
27
+ if (self._startAt === undefined)
28
+ self._startAt = process.hrtime();
29
+ }
30
+ /**
31
+ * Wrap res in a Proxy that delegates unknown properties/methods to the raw Node response.
32
+ * Only delegates when the value exists and is callable; never calls .bind on undefined.
33
+ */
34
+ function responseProxy(res, raw) {
35
+ return new Proxy(res, {
36
+ get(target, prop) {
37
+ if (Object.prototype.hasOwnProperty.call(target, prop)) {
38
+ const v = Reflect.get(target, prop);
39
+ if (typeof v === 'function')
40
+ return v.bind(target);
41
+ return v;
42
+ }
43
+ const rawVal = Reflect.get(raw, prop);
44
+ if (rawVal === undefined || rawVal === null)
45
+ return rawVal;
46
+ if (typeof rawVal === 'function')
47
+ return rawVal.bind(raw);
48
+ return rawVal;
49
+ },
50
+ has(target, prop) {
51
+ return (Object.prototype.hasOwnProperty.call(target, prop) ||
52
+ Reflect.has(raw, prop));
53
+ },
54
+ });
55
+ }
56
+ /** One-shot adapter: creates a new res per call (e.g. for adaptResponse(reply, req)). */
57
+ function adaptResponse(fastifyReply, _fastifyReq) {
58
+ const raw = fastifyReply.raw;
59
+ const res = Object.create(raw);
60
+ let statusCode = 200;
61
+ res._locals = {};
62
+ // Custom 'finish'/'end' so morgan (on-finished) runs when we've sent, not when OS flushes (keep-alive fix).
63
+ const finishListeners = [];
64
+ let finishEmitted = false;
65
+ res.__efrFinishListeners = finishListeners;
66
+ res.__efrEmitFinish = () => {
67
+ if (finishEmitted)
68
+ return;
69
+ finishEmitted = true;
70
+ res.__efrFinishEmitted = true;
71
+ const toRun = [...finishListeners];
72
+ finishListeners.length = 0;
73
+ toRun.forEach(({ fn }) => {
74
+ try {
75
+ fn();
76
+ }
77
+ catch (_) {
78
+ /* ignore */
79
+ }
80
+ });
81
+ };
82
+ Object.assign(res, {
83
+ on(event, listener) {
84
+ if (event === "finish" || event === "end") {
85
+ finishListeners.push({ fn: listener, once: false });
86
+ return res;
87
+ }
88
+ return raw.on.apply(raw, arguments);
89
+ },
90
+ once(event, listener) {
91
+ if (event === "finish" || event === "end") {
92
+ finishListeners.push({ fn: listener, once: true });
93
+ return res;
94
+ }
95
+ return raw.once.apply(raw, arguments);
96
+ },
97
+ emit(event, ...args) {
98
+ if (event === "finish" || event === "end") {
99
+ res.__efrEmitFinish?.();
100
+ return true;
101
+ }
102
+ return raw.emit.apply(raw, arguments);
103
+ },
104
+ });
105
+ // Emit 'finish' after send so morgan/on-finished run per-request. Use double setImmediate so
106
+ // Fastify's sync send (writeHead + end) has run and res._startAt/status/headers are set (no " - - ms - -").
107
+ // Skip async work when no one listens (hot path: res.json() with no res.on('finish')).
108
+ function afterSend() {
109
+ if (finishListeners.length === 0)
110
+ return;
111
+ setImmediate(() => setImmediate(() => emitFinishNextTick(res)));
112
+ }
113
+ function emitFinishNextTick(r) {
114
+ if (r.__efrEmitFinish)
115
+ r.__efrEmitFinish();
116
+ }
117
+ res.locals = res._locals;
118
+ Object.defineProperty(res, 'headersSent', {
119
+ get() {
120
+ return raw.headersSent;
121
+ },
122
+ configurable: true,
123
+ enumerable: true,
124
+ });
125
+ res.status = function (code) {
126
+ statusCode = code;
127
+ raw.statusCode = code;
128
+ return res;
129
+ };
130
+ res.sendStatus = function (code) {
131
+ raw.statusCode = code;
132
+ const body = STATUS_CODES[code] || String(code);
133
+ fastifyReply.type('text/plain').send(body);
134
+ afterSend();
135
+ return res;
136
+ };
137
+ res.send = function (body) {
138
+ if (body === undefined) {
139
+ raw.end();
140
+ afterSend();
141
+ return res;
142
+ }
143
+ if (typeof body === 'object' && body !== null && !Buffer.isBuffer(body)) {
144
+ fastifyReply.type('application/json').send(body);
145
+ afterSend();
146
+ return res;
147
+ }
148
+ // Express: string defaults to html when Content-Type not set (response.js send())
149
+ if (typeof body === 'string' && !raw.getHeader('Content-Type')) {
150
+ fastifyReply.type('text/html').send(body);
151
+ afterSend();
152
+ return res;
153
+ }
154
+ fastifyReply.send(body);
155
+ afterSend();
156
+ return res;
157
+ };
158
+ res.json = function (body) {
159
+ fastifyReply.type('application/json').send(body);
160
+ afterSend();
161
+ return res;
162
+ };
163
+ res.jsonp = function (body) {
164
+ fastifyReply.type('application/json').send(body);
165
+ afterSend();
166
+ return res;
167
+ };
168
+ res.set = res.header = function (field, value) {
169
+ if (typeof field === 'string') {
170
+ raw.setHeader(field, normalizeHeaderValue(value));
171
+ }
172
+ else {
173
+ for (const [k, v] of Object.entries(field))
174
+ raw.setHeader(k, v);
175
+ }
176
+ return res;
177
+ };
178
+ res.setHeader = function (name, value) {
179
+ raw.setHeader(name, value);
180
+ return res;
181
+ };
182
+ res.removeHeader = function (name) {
183
+ raw.removeHeader(name);
184
+ return res;
185
+ };
186
+ res.append = function (field, value) {
187
+ const prev = raw.getHeader(field);
188
+ const next = prev === undefined
189
+ ? normalizeHeaderValue(value)
190
+ : [prev, value].flat().join(', ');
191
+ raw.setHeader(field, next);
192
+ return res;
193
+ };
194
+ res.get = function (field) {
195
+ const v = raw.getHeader(field);
196
+ return v === undefined ? undefined : (Array.isArray(v) ? v.join(', ') : String(v));
197
+ };
198
+ res.getHeader = function (name) {
199
+ return raw.getHeader(name);
200
+ };
201
+ res.type = res.contentType = function (type) {
202
+ if (!type.includes('/')) {
203
+ const mime = {
204
+ json: 'application/json',
205
+ html: 'text/html',
206
+ text: 'text/plain',
207
+ xml: 'application/xml',
208
+ };
209
+ fastifyReply.type(mime[type] || type);
210
+ }
211
+ else {
212
+ raw.setHeader('Content-Type', type);
213
+ }
214
+ return res;
215
+ };
216
+ res.links = function (links) {
217
+ const link = Object.entries(links)
218
+ .map(([rel, url]) => `<${url}>; rel="${rel}"`)
219
+ .join(', ');
220
+ raw.setHeader('Link', link);
221
+ return res;
222
+ };
223
+ res.attachment = function (filename) {
224
+ if (filename) {
225
+ raw.setHeader('Content-Disposition', `attachment; filename="${filename.replace(/"/g, '\\"')}"`);
226
+ }
227
+ else {
228
+ raw.setHeader('Content-Disposition', 'attachment');
229
+ }
230
+ return res;
231
+ };
232
+ res.location = function (url) {
233
+ if (url === 'back') {
234
+ const ref = _fastifyReq?.raw?.headers?.referrer ||
235
+ _fastifyReq?.raw?.headers?.referer;
236
+ url = ref || '/';
237
+ }
238
+ raw.setHeader('Location', (0, encodeurl_1.default)(url));
239
+ return res;
240
+ };
241
+ res.redirect = function (urlOrStatus, url) {
242
+ let status = 302;
243
+ let target;
244
+ if (typeof urlOrStatus === 'number') {
245
+ status = urlOrStatus;
246
+ target = url;
247
+ }
248
+ else {
249
+ target = urlOrStatus;
250
+ }
251
+ raw.statusCode = status;
252
+ raw.setHeader('Location', target);
253
+ fastifyReply.send();
254
+ return res;
255
+ };
256
+ res.vary = function (field) {
257
+ const v = raw.getHeader('Vary');
258
+ const next = v ? `${v}, ${field}` : field;
259
+ raw.setHeader('Vary', next);
260
+ return res;
261
+ };
262
+ res.cookie = function (name, val, options) {
263
+ const value = typeof val === 'object' ? 'j:' + JSON.stringify(val) : encodeURIComponent(String(val));
264
+ const parts = [`${name}=${value}`];
265
+ if (options) {
266
+ if (options.maxAge != null)
267
+ parts.push(`Max-Age=${options.maxAge}`);
268
+ if (options.path)
269
+ parts.push(`Path=${options.path}`);
270
+ if (options.domain)
271
+ parts.push(`Domain=${options.domain}`);
272
+ if (options.expires)
273
+ parts.push(`Expires=${options.expires.toUTCString()}`);
274
+ if (options.httpOnly)
275
+ parts.push('HttpOnly');
276
+ if (options.secure)
277
+ parts.push('Secure');
278
+ if (options.sameSite)
279
+ parts.push(`SameSite=${options.sameSite}`);
280
+ }
281
+ const setCookie = raw.getHeader('Set-Cookie');
282
+ const prev = setCookie
283
+ ? (Array.isArray(setCookie) ? setCookie.map(String) : [String(setCookie)])
284
+ : [];
285
+ raw.setHeader('Set-Cookie', [...prev, parts.join('; ')]);
286
+ return res;
287
+ };
288
+ res.clearCookie = function (name, options) {
289
+ const opts = { ...options, expires: new Date(0), maxAge: 0 };
290
+ res.cookie(name, '', opts);
291
+ return res;
292
+ };
293
+ res.end = function (chunkOrCb, encodingOrCb, cb) {
294
+ if (typeof chunkOrCb === 'function') {
295
+ raw.end(chunkOrCb);
296
+ }
297
+ else if (chunkOrCb !== undefined && chunkOrCb !== null) {
298
+ const enc = typeof encodingOrCb === 'string' ? encodingOrCb : undefined;
299
+ const done = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
300
+ if (enc !== undefined) {
301
+ raw.end(chunkOrCb, enc, done);
302
+ }
303
+ else {
304
+ raw.end(chunkOrCb, done);
305
+ }
306
+ }
307
+ else {
308
+ raw.end();
309
+ }
310
+ return res;
311
+ };
312
+ const RES_RENDER_HINT = 'res.render is not implemented on the Fastify lane. Wrap this handler with expressLane() so the route runs on the Express lane. See docs/FAST_PRODUCTION_CHECKLIST.md';
313
+ res.render = function (_view, _locals, _callback) {
314
+ if (process.env.NODE_ENV !== 'production') {
315
+ console.warn(`[express-fastify-runtime] ${RES_RENDER_HINT}`);
316
+ }
317
+ if (!raw.headersSent) {
318
+ fastifyReply.status(501).type('application/json').send({
319
+ error: 'res.render is not implemented on the Fastify lane',
320
+ hint: 'Wrap this handler with expressLane() so the route runs on the Express lane.',
321
+ docs: 'docs/FAST_PRODUCTION_CHECKLIST.md',
322
+ });
323
+ }
324
+ return res;
325
+ };
326
+ return responseProxy(res, raw);
327
+ }
328
+ /** Tell the middleware/route chain that this response has ended (so it stops waiting on next()). */
329
+ function signalEnd(self) {
330
+ if (self._onEnd)
331
+ self._onEnd();
332
+ }
333
+ /**
334
+ * Take over the response so we can write to the raw Node stream directly (streaming/SSE,
335
+ * res.write/res.writeHead/res.end). reply.hijack() tells Fastify NOT to send its own response,
336
+ * so the handler returning undefined won't trigger a double-send. Idempotent.
337
+ */
338
+ function ensureHijacked(self) {
339
+ if (self._hijacked)
340
+ return;
341
+ self._hijacked = true;
342
+ self._reply.hijack();
343
+ }
344
+ /**
345
+ * Shared response prototype: all methods/getters defined ONCE. Per-request state
346
+ * (_reply, _locals) lives on each instance and is read via `this`, so concurrent
347
+ * requests never share mutable state. This is the correctness-critical difference
348
+ * from the old single-shared-object adapter.
349
+ *
350
+ * Lifecycle events (finish/end/close/error) are delegated to the REAL Node response
351
+ * (reply.raw). This is how morgan/on-finished behave on plain Express (where res IS the
352
+ * ServerResponse): they fire on the actual response 'finish', so status, headers,
353
+ * content-length and res._startAt are all set when the log line is written. We do NOT
354
+ * synthesize 'finish' (the old setImmediate approach raced ahead of the real flush under
355
+ * concurrency, producing "- - ms - -" log lines).
356
+ */
357
+ const responseProto = {
358
+ get locals() {
359
+ const self = this;
360
+ return self._locals ?? (self._locals = {});
361
+ },
362
+ set locals(v) {
363
+ this._locals = v;
364
+ },
365
+ get headersSent() {
366
+ return this._reply?.raw.headersSent ?? false;
367
+ },
368
+ get statusCode() {
369
+ return this._reply?.raw.statusCode ?? 200;
370
+ },
371
+ set statusCode(code) {
372
+ this._reply.raw.statusCode = code;
373
+ },
374
+ // Native response fields on-finished/morgan inspect — delegate to the real response.
375
+ get socket() {
376
+ return this._reply?.raw.socket ?? null;
377
+ },
378
+ get connection() {
379
+ return this._reply?.raw.socket ?? null;
380
+ },
381
+ get finished() {
382
+ return this._reply?.raw.writableEnded ?? false;
383
+ },
384
+ get writableEnded() {
385
+ return this._reply?.raw.writableEnded ?? false;
386
+ },
387
+ get writableFinished() {
388
+ return this._reply?.raw.writableFinished ?? false;
389
+ },
390
+ on(event, listener) {
391
+ this._reply.raw.on(event, listener);
392
+ return this;
393
+ },
394
+ addListener(event, listener) {
395
+ this._reply.raw.on(event, listener);
396
+ return this;
397
+ },
398
+ once(event, listener) {
399
+ this._reply.raw.once(event, listener);
400
+ return this;
401
+ },
402
+ prependListener(event, listener) {
403
+ this._reply.raw.prependListener(event, listener);
404
+ return this;
405
+ },
406
+ removeListener(event, listener) {
407
+ this._reply.raw.removeListener(event, listener);
408
+ return this;
409
+ },
410
+ emit(event, ...args) {
411
+ return this._reply.raw.emit(event, ...args);
412
+ },
413
+ status(code) {
414
+ this._reply.raw.statusCode = code;
415
+ return this;
416
+ },
417
+ sendStatus(code) {
418
+ const r = this._reply;
419
+ r.raw.statusCode = code;
420
+ const body = STATUS_CODES[code] || String(code);
421
+ recordStartAt(this);
422
+ r.type('text/plain').send(body);
423
+ signalEnd(this);
424
+ return this;
425
+ },
426
+ send(body) {
427
+ const r = this._reply;
428
+ const raw = r.raw;
429
+ recordStartAt(this);
430
+ if (body === undefined) {
431
+ raw.end();
432
+ }
433
+ else if (typeof body === 'object' && body !== null && !Buffer.isBuffer(body)) {
434
+ r.type('application/json').send(body);
435
+ }
436
+ else if (typeof body === 'string' && !raw.getHeader('Content-Type')) {
437
+ r.type('text/html').send(body);
438
+ }
439
+ else {
440
+ r.send(body);
441
+ }
442
+ signalEnd(this);
443
+ return this;
444
+ },
445
+ json(body) {
446
+ recordStartAt(this);
447
+ this._reply.type('application/json').send(body);
448
+ signalEnd(this);
449
+ return this;
450
+ },
451
+ jsonp(body) {
452
+ recordStartAt(this);
453
+ this._reply.type('application/json').send(body);
454
+ signalEnd(this);
455
+ return this;
456
+ },
457
+ set(field, value) {
458
+ const raw = this._reply.raw;
459
+ if (typeof field === 'string') {
460
+ raw.setHeader(field, normalizeHeaderValue(value));
461
+ }
462
+ else {
463
+ for (const [k, v] of Object.entries(field))
464
+ raw.setHeader(k, v);
465
+ }
466
+ return this;
467
+ },
468
+ header(field, value) {
469
+ return this.set(field, value);
470
+ },
471
+ setHeader(name, value) {
472
+ this._reply.raw.setHeader(name, value);
473
+ return this;
474
+ },
475
+ removeHeader(name) {
476
+ this._reply.raw.removeHeader(name);
477
+ return this;
478
+ },
479
+ append(field, value) {
480
+ const raw = this._reply.raw;
481
+ const prev = raw.getHeader(field);
482
+ const next = prev === undefined
483
+ ? normalizeHeaderValue(value)
484
+ : [prev, value].flat().join(', ');
485
+ raw.setHeader(field, next);
486
+ return this;
487
+ },
488
+ get(field) {
489
+ const v = this._reply.raw.getHeader(field);
490
+ return v === undefined ? undefined : (Array.isArray(v) ? v.join(', ') : String(v));
491
+ },
492
+ getHeader(name) {
493
+ return this._reply.raw.getHeader(name);
494
+ },
495
+ type(type) {
496
+ if (!type.includes('/')) {
497
+ const mime = {
498
+ json: 'application/json',
499
+ html: 'text/html',
500
+ text: 'text/plain',
501
+ xml: 'application/xml',
502
+ };
503
+ this._reply.type(mime[type] || type);
504
+ }
505
+ else {
506
+ this._reply.raw.setHeader('Content-Type', type);
507
+ }
508
+ return this;
509
+ },
510
+ contentType(type) {
511
+ return this.type(type);
512
+ },
513
+ links(links) {
514
+ const link = Object.entries(links)
515
+ .map(([rel, url]) => `<${url}>; rel="${rel}"`)
516
+ .join(', ');
517
+ this._reply.raw.setHeader('Link', link);
518
+ return this;
519
+ },
520
+ attachment(filename) {
521
+ const raw = this._reply.raw;
522
+ if (filename) {
523
+ raw.setHeader('Content-Disposition', `attachment; filename="${filename.replace(/"/g, '\\"')}"`);
524
+ }
525
+ else {
526
+ raw.setHeader('Content-Disposition', 'attachment');
527
+ }
528
+ return this;
529
+ },
530
+ location(url) {
531
+ if (url === 'back') {
532
+ const req = this._req;
533
+ const ref = req?.raw?.headers?.referrer || req?.raw?.headers?.referer;
534
+ url = ref || '/';
535
+ }
536
+ this._reply.raw.setHeader('Location', (0, encodeurl_1.default)(url));
537
+ return this;
538
+ },
539
+ redirect(urlOrStatus, url) {
540
+ const r = this._reply;
541
+ let status = 302;
542
+ let target;
543
+ if (typeof urlOrStatus === 'number') {
544
+ status = urlOrStatus;
545
+ target = url;
546
+ }
547
+ else {
548
+ target = urlOrStatus;
549
+ }
550
+ recordStartAt(this);
551
+ r.raw.statusCode = status;
552
+ r.raw.setHeader('Location', target);
553
+ r.send();
554
+ signalEnd(this);
555
+ return this;
556
+ },
557
+ vary(field) {
558
+ const raw = this._reply.raw;
559
+ const v = raw.getHeader('Vary');
560
+ const next = v ? `${v}, ${field}` : field;
561
+ raw.setHeader('Vary', next);
562
+ return this;
563
+ },
564
+ cookie(name, val, options) {
565
+ const value = typeof val === 'object' ? 'j:' + JSON.stringify(val) : encodeURIComponent(String(val));
566
+ const parts = [`${name}=${value}`];
567
+ if (options) {
568
+ if (options.maxAge != null)
569
+ parts.push(`Max-Age=${options.maxAge}`);
570
+ if (options.path)
571
+ parts.push(`Path=${options.path}`);
572
+ if (options.domain)
573
+ parts.push(`Domain=${options.domain}`);
574
+ if (options.expires)
575
+ parts.push(`Expires=${options.expires.toUTCString()}`);
576
+ if (options.httpOnly)
577
+ parts.push('HttpOnly');
578
+ if (options.secure)
579
+ parts.push('Secure');
580
+ if (options.sameSite)
581
+ parts.push(`SameSite=${options.sameSite}`);
582
+ }
583
+ const raw = this._reply.raw;
584
+ const setCookie = raw.getHeader('Set-Cookie');
585
+ const prev = setCookie
586
+ ? (Array.isArray(setCookie) ? setCookie.map(String) : [String(setCookie)])
587
+ : [];
588
+ raw.setHeader('Set-Cookie', [...prev, parts.join('; ')]);
589
+ return this;
590
+ },
591
+ clearCookie(name, options) {
592
+ const opts = { ...options, expires: new Date(0), maxAge: 0 };
593
+ this.cookie(name, '', opts);
594
+ return this;
595
+ },
596
+ // Streaming / SSE: write directly to the raw Node response. First write hijacks the reply so
597
+ // Fastify doesn't also try to send. Mirrors Express where res IS the ServerResponse.
598
+ write(chunk, encoding, cb) {
599
+ ensureHijacked(this);
600
+ return this._reply.raw.write(chunk, encoding, cb);
601
+ },
602
+ writeHead(statusCode, ...rest) {
603
+ ensureHijacked(this);
604
+ this._reply.raw.writeHead(statusCode, ...rest);
605
+ return this;
606
+ },
607
+ flushHeaders() {
608
+ ensureHijacked(this);
609
+ this._reply.raw.flushHeaders();
610
+ return this;
611
+ },
612
+ end(chunkOrCb, encodingOrCb, cb) {
613
+ ensureHijacked(this);
614
+ const raw = this._reply.raw;
615
+ if (typeof chunkOrCb === 'function') {
616
+ raw.end(chunkOrCb);
617
+ }
618
+ else if (chunkOrCb !== undefined && chunkOrCb !== null) {
619
+ const enc = typeof encodingOrCb === 'string' ? encodingOrCb : undefined;
620
+ const done = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
621
+ if (enc !== undefined) {
622
+ raw.end(chunkOrCb, enc, done);
623
+ }
624
+ else {
625
+ raw.end(chunkOrCb, done);
626
+ }
627
+ }
628
+ else {
629
+ raw.end();
630
+ }
631
+ signalEnd(this);
632
+ return this;
633
+ },
634
+ render(_view, _locals, _callback) {
635
+ const RES_RENDER_HINT = 'res.render is not implemented on the Fastify lane. Wrap this handler with expressLane() so the route runs on the Express lane. See docs/FAST_PRODUCTION_CHECKLIST.md';
636
+ if (process.env.NODE_ENV !== 'production') {
637
+ console.warn(`[express-fastify-runtime] ${RES_RENDER_HINT}`);
638
+ }
639
+ const r = this._reply;
640
+ if (!r.raw.headersSent) {
641
+ r.status(501).type('application/json').send({
642
+ error: 'res.render is not implemented on the Fastify lane',
643
+ hint: 'Wrap this handler with expressLane() so the route runs on the Express lane.',
644
+ docs: 'docs/FAST_PRODUCTION_CHECKLIST.md',
645
+ });
646
+ }
647
+ return this;
648
+ },
649
+ };
650
+ /**
651
+ * Returns a response adapter that builds a FRESH Express-like res per request.
652
+ *
653
+ * CRITICAL: a new instance is created per request (methods shared via prototype,
654
+ * state per instance). A single shared/mutated res object corrupts concurrent async
655
+ * requests — a later request would overwrite _reply while an earlier request (or its
656
+ * deferred morgan finish listener) is still using it.
657
+ */
658
+ function createResponseAdapter() {
659
+ return (fastifyReply, fastifyReq) => {
660
+ const res = Object.create(responseProto);
661
+ res._reply = fastifyReply;
662
+ res._req = fastifyReq;
663
+ res._locals = null; // lazily created on first res.locals access
664
+ return res;
665
+ };
666
+ }
667
+ //# sourceMappingURL=response.js.map