@web-widget/node 3.0.0-beta.2 → 3.0.0-beta.5

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 (3) hide show
  1. package/dist/node.d.ts +18 -3
  2. package/dist/node.js +662 -116
  3. package/package.json +19 -10
package/dist/node.d.ts CHANGED
@@ -1,7 +1,20 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { RequestOptions, NodeHandler } from '@edge-runtime/node-utils';
2
+ import primitives from '@edge-runtime/primitives';
3
3
 
4
- type WebHandler = (req: Request, env: Record<string, unknown>, event: FetchEvent) => Promise<Response> | Response | null | undefined;
4
+ interface BuildDependencies {
5
+ Headers: typeof Headers;
6
+ ReadableStream: typeof ReadableStream;
7
+ Request: typeof Request;
8
+ Uint8Array: typeof Uint8Array;
9
+ FetchEvent: typeof primitives.FetchEvent;
10
+ }
11
+ interface RequestOptions {
12
+ defaultOrigin: string;
13
+ }
14
+ type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void> | void;
15
+ type EdgeFetchEvent = InstanceType<BuildDependencies['FetchEvent']>;
16
+
17
+ type WebHandler = (req: Request, env: Record<string, unknown>, event: EdgeFetchEvent) => Promise<Response> | Response | null | undefined;
5
18
  interface NodeAdapterOptions extends RequestOptions {
6
19
  /** builds a transformer, using Node.js@18 globals, and a base url for URL constructor. */
7
20
  defaultOrigin: string;
@@ -17,5 +30,7 @@ declare class NodeAdapter {
17
30
  get middleware(): Middleware;
18
31
  get handler(): NodeHandler;
19
32
  }
33
+ declare function toServerResponse(webResponse: Response | null | undefined, serverResponse: ServerResponse): void | Promise<void>;
34
+ declare function buildToNodeHandler(dependencies: BuildDependencies, options: RequestOptions): (webHandler: WebHandler) => NodeHandler;
20
35
 
21
- export { type Middleware, type NodeAdapterOptions, NodeAdapter as default };
36
+ export { type Middleware, type NodeAdapterOptions, buildToNodeHandler, NodeAdapter as default, toServerResponse };
package/dist/node.js CHANGED
@@ -1,11 +1,3 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
-
9
1
  // src/global.ts
10
2
  import primitives from "@edge-runtime/primitives";
11
3
  if (!Reflect.get(global, "DISABLE_INSTALL_MCA_SHIMS")) {
@@ -17,15 +9,28 @@ if (!Reflect.get(global, "DISABLE_INSTALL_MCA_SHIMS")) {
17
9
  }
18
10
 
19
11
  // src/adapter.ts
20
- import {
21
- buildToFetchEvent,
22
- mergeIntoServerResponse,
23
- toOutgoingHeaders
24
- } from "@edge-runtime/node-utils";
25
12
  import primitives2 from "@edge-runtime/primitives";
26
13
 
14
+ // src/node-utils.ts
15
+ function buildToFetchEvent(dependencies2) {
16
+ return function toFetchEvent(request) {
17
+ return new dependencies2.FetchEvent(request);
18
+ };
19
+ }
20
+ function buildToHeaders({ Headers: HeadersCtor }) {
21
+ return function toHeaders(nodeHeaders) {
22
+ const headers = new HeadersCtor();
23
+ for (const [key, value] of Object.entries(nodeHeaders)) {
24
+ const values = Array.isArray(value) ? value : [value];
25
+ for (const item of values) {
26
+ if (item !== void 0) headers.append(key, item);
27
+ }
28
+ }
29
+ return headers;
30
+ };
31
+ }
32
+
27
33
  // src/incoming-request.ts
28
- import { buildToHeaders } from "@edge-runtime/node-utils";
29
34
  function isHttp2PseudoHeader(name) {
30
35
  return name.startsWith(":");
31
36
  }
@@ -50,29 +55,6 @@ function sanitizeIncomingHttpHeaders(headers) {
50
55
  }
51
56
  return sanitized;
52
57
  }
53
- function computeRequestOrigin(headers, defaultOrigin) {
54
- var _a;
55
- const authority = (_a = headerValue(headers.host)) != null ? _a : headerValue(headers[":authority"]);
56
- if (!authority) {
57
- return defaultOrigin;
58
- }
59
- let protocol = "http";
60
- if (defaultOrigin) {
61
- try {
62
- protocol = new URL(defaultOrigin).protocol.replace(":", "") || "http";
63
- } catch {
64
- }
65
- }
66
- const scheme = headerValue(headers[":scheme"]);
67
- if (scheme === "http" || scheme === "https") {
68
- protocol = scheme;
69
- }
70
- const [, port] = authority.split(":");
71
- if (port === "443") {
72
- protocol = "https";
73
- }
74
- return `${protocol}://${authority}`;
75
- }
76
58
  function buildToReadableStream(dependencies2) {
77
59
  const { ReadableStream: ReadableStreamCtor, Uint8Array: Uint8ArrayCtor } = dependencies2;
78
60
  return function toReadableStream(stream) {
@@ -99,63 +81,401 @@ function buildToReadableStream(dependencies2) {
99
81
  });
100
82
  };
101
83
  }
102
- function buildToRequestWithHttp2Compat(dependencies2) {
84
+ function buildToRequest(dependencies2) {
103
85
  const toHeaders = buildToHeaders(dependencies2);
104
86
  const toReadableStream = buildToReadableStream(dependencies2);
105
87
  const { Request: Request2 } = dependencies2;
88
+ const getRequestHeaders = (incomingHeaders) => {
89
+ const headers = incomingHeaders[":authority"] ? sanitizeIncomingHttpHeaders(incomingHeaders) : incomingHeaders;
90
+ if (incomingHeaders[":authority"] || Array.isArray(headers["set-cookie"])) {
91
+ return toHeaders(headers);
92
+ }
93
+ return headers;
94
+ };
95
+ let cachedDefaultOrigin = "";
96
+ let cachedDefaultProtocol = "http";
97
+ const getDefaultProtocol = (defaultOrigin) => {
98
+ if (defaultOrigin === cachedDefaultOrigin) {
99
+ return cachedDefaultProtocol;
100
+ }
101
+ cachedDefaultOrigin = defaultOrigin;
102
+ try {
103
+ cachedDefaultProtocol = new URL(defaultOrigin).protocol.replace(":", "") || "http";
104
+ } catch {
105
+ cachedDefaultProtocol = "http";
106
+ }
107
+ return cachedDefaultProtocol;
108
+ };
106
109
  return function toRequest(request, options) {
107
- var _a, _b;
108
- const base = computeRequestOrigin(request.headers, options.defaultOrigin);
109
- const headers = sanitizeIncomingHttpHeaders(request.headers);
110
- return new Request2(
111
- String(
112
- ((_a = request.url) == null ? void 0 : _a.startsWith("//")) ? new URL(base + request.url) : new URL(request.url || "/", base)
113
- ),
114
- {
115
- method: request.method,
116
- headers: toHeaders(headers),
117
- body: !["HEAD", "GET"].includes((_b = request.method) != null ? _b : "") ? toReadableStream(request) : null
110
+ const requestHeaders = request.headers;
111
+ const authority = headerValue(requestHeaders.host) ?? headerValue(requestHeaders[":authority"]);
112
+ let protocol = getDefaultProtocol(options.defaultOrigin);
113
+ const scheme = headerValue(requestHeaders[":scheme"]);
114
+ if (scheme === "http" || scheme === "https") {
115
+ protocol = scheme;
116
+ } else if (authority?.endsWith(":443")) {
117
+ protocol = "https";
118
+ }
119
+ const base = authority ? `${protocol}://${authority}` : options.defaultOrigin;
120
+ const rawUrl = request.url || "/";
121
+ const url = authority && rawUrl.startsWith("/") ? base + rawUrl : String(new URL(rawUrl, base));
122
+ const method = request.method || "GET";
123
+ const headers = getRequestHeaders(requestHeaders);
124
+ if (method === "GET") {
125
+ return new Request2(url, { headers });
126
+ }
127
+ if (method === "HEAD") {
128
+ return new Request2(url, { method, headers });
129
+ }
130
+ return new Request2(url, {
131
+ method,
132
+ headers,
133
+ body: toReadableStream(request),
134
+ duplex: "half"
135
+ });
136
+ };
137
+ }
138
+ var buildToRequestWithHttp2Compat = buildToRequest;
139
+ function buildToRequestSource(dependencies2, options) {
140
+ const toRequest = buildToRequest(dependencies2);
141
+ const incomingKey = Symbol("incomingMessage");
142
+ const nativeKey = Symbol("nativeRequest");
143
+ const sourcePrototype = {
144
+ method: "GET",
145
+ url: "",
146
+ toRequest() {
147
+ return this[nativeKey] ??= toRequest(this[incomingKey], options);
148
+ },
149
+ cancel() {
150
+ const request = this[nativeKey];
151
+ if (request) {
152
+ const body = request.body;
153
+ if (body && !body.locked) body.cancel().catch(() => {
154
+ });
155
+ } else if (this.method !== "GET" && this.method !== "HEAD") {
156
+ this[incomingKey].destroy();
118
157
  }
119
- );
158
+ }
159
+ };
160
+ let cachedDefaultOrigin = "";
161
+ let cachedDefaultProtocol = "http";
162
+ const getDefaultProtocol = (defaultOrigin) => {
163
+ if (defaultOrigin === cachedDefaultOrigin) return cachedDefaultProtocol;
164
+ cachedDefaultOrigin = defaultOrigin;
165
+ try {
166
+ cachedDefaultProtocol = new URL(defaultOrigin).protocol.replace(":", "") || "http";
167
+ } catch {
168
+ cachedDefaultProtocol = "http";
169
+ }
170
+ return cachedDefaultProtocol;
171
+ };
172
+ return function toRequestSource(incomingMessage) {
173
+ const headers = incomingMessage.headers;
174
+ const authority = headerValue(headers.host) ?? headerValue(headers[":authority"]);
175
+ let protocol = getDefaultProtocol(options.defaultOrigin);
176
+ const scheme = headerValue(headers[":scheme"]);
177
+ if (scheme === "http" || scheme === "https") protocol = scheme;
178
+ else if (authority?.endsWith(":443")) protocol = "https";
179
+ const base = authority ? `${protocol}://${authority}` : options.defaultOrigin;
180
+ const rawUrl = incomingMessage.url || "/";
181
+ const source = Object.create(sourcePrototype);
182
+ source.method = incomingMessage.method || "GET";
183
+ source.url = authority && rawUrl.startsWith("/") ? base + rawUrl : String(new URL(rawUrl, base));
184
+ source[incomingKey] = incomingMessage;
185
+ return source;
120
186
  };
121
187
  }
122
188
 
189
+ // src/response.ts
190
+ var RESPONSE_CACHE = Symbol.for("@web-widget/node.response-cache");
191
+ var installed = false;
192
+ var HEADER_NAME = /^[!#$%&'*+\-.^`|~\w]+$/;
193
+ function toUnsignedShort(value) {
194
+ if (typeof value === "bigint" || typeof value === "symbol") {
195
+ throw new TypeError("Status is not a number");
196
+ }
197
+ const number = Number(value);
198
+ if (!Number.isFinite(number) || number === 0) return 0;
199
+ const integer = Math.trunc(number);
200
+ return (integer % 65536 + 65536) % 65536;
201
+ }
202
+ function toByteString(value, name) {
203
+ if (typeof value === "symbol") {
204
+ throw new TypeError(`${name} is not a valid ByteString`);
205
+ }
206
+ const result = value === void 0 ? "" : String(value);
207
+ for (let index = 0; index < result.length; index++) {
208
+ if (result.charCodeAt(index) > 255) {
209
+ throw new TypeError(`${name} is not a valid ByteString`);
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+ function normalizeHeaderValue(value, name) {
215
+ let start = 0;
216
+ let end = value.length;
217
+ for (let index = 0; index < value.length; index++) {
218
+ const code = value.charCodeAt(index);
219
+ if (code > 255)
220
+ throw new TypeError("Header value is not a valid ByteString");
221
+ if (code === 0 || code === 10 || code === 13) {
222
+ throw new TypeError(`Invalid header value for ${name}`);
223
+ }
224
+ if (index === start && (code === 9 || code === 32)) start++;
225
+ }
226
+ while (end > start) {
227
+ const code = value.charCodeAt(end - 1);
228
+ if (code !== 9 && code !== 32) break;
229
+ end--;
230
+ }
231
+ return start === 0 && end === value.length ? value : value.slice(start, end);
232
+ }
233
+ function snapshotCachedHeaders(headers, cache) {
234
+ if (!headers) return;
235
+ if (headers instanceof Headers || Array.isArray(headers)) {
236
+ cache.headers = new Headers(headers);
237
+ return;
238
+ }
239
+ if (Symbol.iterator in headers) {
240
+ cache.headers = new Headers(headers);
241
+ return;
242
+ }
243
+ const prototype = Object.getPrototypeOf(headers);
244
+ if (prototype !== Object.prototype && prototype !== null) {
245
+ cache.headers = new Headers(headers);
246
+ return;
247
+ }
248
+ let firstName;
249
+ let firstValue;
250
+ let snapshot;
251
+ for (const name in headers) {
252
+ if (!Object.hasOwn(headers, name)) continue;
253
+ const value = headers[name];
254
+ if (typeof value !== "string") {
255
+ cache.headers = new Headers(headers);
256
+ return;
257
+ }
258
+ if (!HEADER_NAME.test(name)) {
259
+ throw new TypeError(`Invalid header name: ${name}`);
260
+ }
261
+ const normalizedName = name.toLowerCase();
262
+ if (normalizedName === firstName || snapshot !== void 0 && normalizedName in snapshot) {
263
+ cache.headers = new Headers(headers);
264
+ return;
265
+ }
266
+ const normalizedValue = normalizeHeaderValue(value, name);
267
+ if (firstName === void 0) {
268
+ firstName = normalizedName;
269
+ firstValue = normalizedValue;
270
+ } else {
271
+ snapshot ??= Object.assign(/* @__PURE__ */ Object.create(null), {
272
+ [firstName]: firstValue
273
+ });
274
+ snapshot[normalizedName] = normalizedValue;
275
+ }
276
+ }
277
+ if (snapshot) cache.headers = snapshot;
278
+ else if (firstName !== void 0) {
279
+ cache.headerName = firstName;
280
+ cache.headerValue = firstValue;
281
+ }
282
+ }
283
+ function getCachedResponse(response) {
284
+ return response[RESPONSE_CACHE];
285
+ }
286
+ function installCachedResponse() {
287
+ if (installed) return;
288
+ installed = true;
289
+ if (Reflect.get(globalThis, "DISABLE_INSTALL_MCA_SHIMS")) return;
290
+ const NativeResponse = globalThis.Response;
291
+ const defaultContentType = "text/plain;charset=UTF-8";
292
+ const getNative = Symbol("getNativeResponse");
293
+ class CachedWebResponse {
294
+ #body;
295
+ #init;
296
+ #native;
297
+ constructor(body = null, init) {
298
+ const status = init?.status === void 0 ? 200 : toUnsignedShort(init.status);
299
+ if (status < 200 || status > 599) {
300
+ throw new RangeError(
301
+ 'init["status"] must be in the range of 200 to 599, inclusive.'
302
+ );
303
+ }
304
+ const statusText = toByteString(init?.statusText, "statusText");
305
+ if (/[^\t\x20-\x7e\x80-\xff]/.test(statusText)) {
306
+ throw new TypeError("Invalid statusText");
307
+ }
308
+ if (body !== null && (status === 204 || status === 205 || status === 304)) {
309
+ throw new TypeError(
310
+ "Response constructor: Invalid response status code"
311
+ );
312
+ }
313
+ if (body === null || typeof body === "string" || body instanceof Uint8Array || body instanceof Blob) {
314
+ const cachedBody = body instanceof Uint8Array ? new Uint8Array(body) : body;
315
+ this.#body = cachedBody;
316
+ const cache = {
317
+ body: cachedBody,
318
+ status,
319
+ statusText
320
+ };
321
+ snapshotCachedHeaders(init?.headers, cache);
322
+ this[RESPONSE_CACHE] = cache;
323
+ } else {
324
+ const headers = new Headers(init?.headers);
325
+ const normalizedInit = { ...init, headers, status, statusText };
326
+ this.#body = body;
327
+ this.#init = normalizedInit;
328
+ this.#native = new NativeResponse(body, normalizedInit);
329
+ }
330
+ }
331
+ [getNative]() {
332
+ if (!this.#native) {
333
+ const cache = this[RESPONSE_CACHE];
334
+ if (cache) {
335
+ delete this[RESPONSE_CACHE];
336
+ this.#init = {
337
+ headers: cache.headers ?? (cache.headerName ? { [cache.headerName]: cache.headerValue } : void 0),
338
+ status: cache.status,
339
+ statusText: cache.statusText
340
+ };
341
+ }
342
+ this.#native = new NativeResponse(this.#body, this.#init);
343
+ }
344
+ return this.#native;
345
+ }
346
+ get headers() {
347
+ const cache = this[RESPONSE_CACHE];
348
+ if (!cache) return this[getNative]().headers;
349
+ if (cache.headerName) {
350
+ cache.headers = new Headers({
351
+ [cache.headerName]: cache.headerValue
352
+ });
353
+ cache.headerName = void 0;
354
+ cache.headerValue = void 0;
355
+ } else if (!(cache.headers instanceof Headers)) {
356
+ cache.headers = new Headers(cache.headers);
357
+ }
358
+ if (typeof cache.body === "string" && !cache.headers.has("content-type")) {
359
+ cache.headers.set("content-type", defaultContentType);
360
+ }
361
+ return cache.headers;
362
+ }
363
+ get status() {
364
+ return this[RESPONSE_CACHE]?.status ?? this[getNative]().status;
365
+ }
366
+ get ok() {
367
+ const status = this.status;
368
+ return status >= 200 && status < 300;
369
+ }
370
+ }
371
+ for (const property of [
372
+ "body",
373
+ "bodyUsed",
374
+ "redirected",
375
+ "statusText",
376
+ "type",
377
+ "url"
378
+ ]) {
379
+ Object.defineProperty(CachedWebResponse.prototype, property, {
380
+ get() {
381
+ return this[getNative]()[property];
382
+ }
383
+ });
384
+ }
385
+ for (const method of [
386
+ "arrayBuffer",
387
+ "blob",
388
+ "bytes",
389
+ "clone",
390
+ "formData",
391
+ "json",
392
+ "text"
393
+ ]) {
394
+ if (typeof NativeResponse.prototype[method] !== "function") {
395
+ continue;
396
+ }
397
+ Object.defineProperty(CachedWebResponse.prototype, method, {
398
+ value() {
399
+ const native = this[getNative]();
400
+ return native[method].call(native);
401
+ }
402
+ });
403
+ }
404
+ Object.defineProperty(CachedWebResponse, "json", {
405
+ value(data, init) {
406
+ const body = JSON.stringify(data);
407
+ if (body === void 0) {
408
+ throw new TypeError("The data is not JSON serializable");
409
+ }
410
+ const headers = new Headers(init?.headers);
411
+ if (!headers.has("content-type")) {
412
+ headers.set("content-type", "application/json");
413
+ }
414
+ return new CachedWebResponse(body, { ...init, headers });
415
+ },
416
+ configurable: true,
417
+ writable: true
418
+ });
419
+ Object.defineProperty(CachedWebResponse, Symbol.hasInstance, {
420
+ value(value) {
421
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
422
+ return false;
423
+ }
424
+ return Object.prototype.isPrototypeOf.call(NativeResponse.prototype, value) || Object.prototype.isPrototypeOf.call(CachedWebResponse.prototype, value);
425
+ }
426
+ });
427
+ Object.setPrototypeOf(CachedWebResponse, NativeResponse);
428
+ Object.setPrototypeOf(CachedWebResponse.prototype, NativeResponse.prototype);
429
+ Object.defineProperty(CachedWebResponse, "name", { value: "Response" });
430
+ globalThis.Response = CachedWebResponse;
431
+ }
432
+
123
433
  // src/adapter.ts
434
+ var REQUEST_SOURCE_HANDLER = Symbol.for(
435
+ "@web-widget/web-router.request-source-handler"
436
+ );
437
+ var NODE_EXECUTION_CONTEXT = {
438
+ waitUntil(promise) {
439
+ void Promise.resolve(promise).catch((error) => {
440
+ console.error("Background task failed.", error);
441
+ });
442
+ },
443
+ passThroughOnException() {
444
+ }
445
+ };
124
446
  var dependencies = {
125
447
  Headers,
126
448
  ReadableStream,
127
- Request: class extends Request {
128
- constructor(input, init) {
129
- super(input, addDuplexToInit(init));
130
- }
131
- },
449
+ Request,
132
450
  Uint8Array,
133
451
  FetchEvent: primitives2.FetchEvent
134
452
  };
135
- var _handler, _middleware;
136
453
  var NodeAdapter = class {
454
+ #handler;
455
+ #middleware;
137
456
  constructor(webRouter, options) {
138
- __privateAdd(this, _handler);
139
- __privateAdd(this, _middleware);
457
+ installCachedResponse();
458
+ const requestSourceHandler = Reflect.get(
459
+ webRouter,
460
+ REQUEST_SOURCE_HANDLER
461
+ );
462
+ if (typeof requestSourceHandler === "function") {
463
+ const handler2 = requestSourceHandler.bind(webRouter);
464
+ this.#handler = buildToNodeSourceHandler(dependencies, options)(handler2);
465
+ this.#middleware = toRequestSourceMiddleware(handler2, options);
466
+ return;
467
+ }
140
468
  const handler = webRouter.handler.bind(webRouter);
141
- __privateSet(this, _handler, buildToNodeHandler(dependencies, options)(handler));
142
- __privateSet(this, _middleware, toMiddleware(handler, options));
469
+ this.#handler = buildToNodeHandler(dependencies, options)(handler);
470
+ this.#middleware = toMiddleware(handler, options);
143
471
  }
144
472
  get middleware() {
145
- return __privateGet(this, _middleware);
473
+ return this.#middleware;
146
474
  }
147
475
  get handler() {
148
- return __privateGet(this, _handler);
476
+ return this.#handler;
149
477
  }
150
478
  };
151
- _handler = new WeakMap();
152
- _middleware = new WeakMap();
153
- function addDuplexToInit(init) {
154
- if (typeof init === "undefined" || typeof init === "object") {
155
- return { duplex: "half", ...init };
156
- }
157
- return init;
158
- }
159
479
  function isHttp2ServerResponse(serverResponse) {
160
480
  if ("stream" in serverResponse && serverResponse.stream) return true;
161
481
  const httpVersionMajor = serverResponse.httpVersionMajor;
@@ -170,8 +490,9 @@ function toMiddleware(webHandler, options) {
170
490
  const toRequest = buildToRequestWithHttp2Compat(dependencies);
171
491
  const toFetchEvent = buildToFetchEvent(dependencies);
172
492
  return async function middleware(incomingMessage, serverResponse, next) {
173
- const request = toRequest(incomingMessage, options);
493
+ let request;
174
494
  try {
495
+ request = toRequest(incomingMessage, options);
175
496
  const env = process.env;
176
497
  const event = toFetchEvent(request);
177
498
  const webResponse = await webHandler(request, env, event);
@@ -189,11 +510,40 @@ function toMiddleware(webHandler, options) {
189
510
  next(error);
190
511
  }
191
512
  } finally {
192
- cancelRequestBody(request);
513
+ if (request) cancelRequestBody(request);
514
+ }
515
+ };
516
+ }
517
+ function toRequestSourceMiddleware(webHandler, options) {
518
+ const toRequestSource = buildToRequestSource(dependencies, options);
519
+ return async function middleware(incomingMessage, serverResponse, next) {
520
+ let source;
521
+ try {
522
+ source = toRequestSource(incomingMessage);
523
+ const webResponse = await webHandler(
524
+ source,
525
+ process.env,
526
+ NODE_EXECUTION_CONTEXT
527
+ );
528
+ await toServerResponse(webResponse, serverResponse);
529
+ if (!serverResponse.headersSent && !serverResponse.writableEnded) {
530
+ await next();
531
+ }
532
+ } catch (error) {
533
+ if (sendServerError(
534
+ serverResponse,
535
+ error instanceof Error ? error.message : void 0
536
+ )) {
537
+ next();
538
+ } else {
539
+ next(error);
540
+ }
541
+ } finally {
542
+ source?.cancel();
193
543
  }
194
544
  };
195
545
  }
196
- async function toServerResponse(webResponse, serverResponse) {
546
+ function toServerResponse(webResponse, serverResponse) {
197
547
  if (!checkWritable(serverResponse)) {
198
548
  return;
199
549
  }
@@ -201,15 +551,23 @@ async function toServerResponse(webResponse, serverResponse) {
201
551
  serverResponse.end();
202
552
  return;
203
553
  }
554
+ const cached = getCachedResponse(webResponse);
555
+ if (cached) {
556
+ return writeCachedResponse(cached, serverResponse);
557
+ }
204
558
  const isHttp2 = isHttp2ServerResponse(serverResponse);
205
- const headers = toOutgoingHeaders(webResponse.headers);
559
+ const headers = {};
560
+ const responseHeaders = webResponse.headers;
561
+ const setCookies = responseHeaders.getSetCookie?.();
562
+ for (const [name, value] of responseHeaders) {
563
+ headers[name] = name === "set-cookie" && setCookies?.length ? setCookies : value;
564
+ }
206
565
  const status = webResponse.status;
207
566
  if (!serverResponse.headersSent) {
208
- mergeIntoServerResponse(headers, serverResponse);
209
- serverResponse.statusCode = status;
210
567
  if (!isHttp2) {
211
568
  setHttp1StatusMessage(serverResponse, webResponse.statusText);
212
569
  }
570
+ serverResponse.writeHead(status, headers);
213
571
  } else if (!isHttp2) {
214
572
  serverResponse.statusCode = status;
215
573
  setHttp1StatusMessage(serverResponse, webResponse.statusText);
@@ -218,53 +576,140 @@ async function toServerResponse(webResponse, serverResponse) {
218
576
  serverResponse.end();
219
577
  return;
220
578
  }
221
- await writeReadableStreamToWritable(webResponse.body, serverResponse);
579
+ return writeReadableStreamToWritable(webResponse.body, serverResponse);
222
580
  }
223
- async function writeReadableStreamToWritable(stream, writable) {
581
+ function writeCachedResponse(response, serverResponse) {
582
+ const { body } = response;
583
+ const cachedHeaders = response.headers;
584
+ if (!cachedHeaders || !Array.isArray(cachedHeaders) && !(cachedHeaders instanceof Headers)) {
585
+ const outgoingHeaders2 = {
586
+ ...cachedHeaders
587
+ };
588
+ if (response.headerName) {
589
+ outgoingHeaders2[response.headerName] = response.headerValue;
590
+ }
591
+ if (typeof body === "string" && !outgoingHeaders2["content-type"]) {
592
+ outgoingHeaders2["content-type"] = "text/plain;charset=UTF-8";
593
+ } else if (body instanceof Blob && body.type && !outgoingHeaders2["content-type"]) {
594
+ outgoingHeaders2["content-type"] = body.type;
595
+ }
596
+ if (!outgoingHeaders2["content-length"]) {
597
+ if (typeof body === "string") {
598
+ outgoingHeaders2["content-length"] = String(Buffer.byteLength(body));
599
+ } else if (body instanceof Uint8Array) {
600
+ outgoingHeaders2["content-length"] = String(body.byteLength);
601
+ } else if (body instanceof Blob) {
602
+ outgoingHeaders2["content-length"] = String(body.size);
603
+ }
604
+ }
605
+ return writeCachedBody(response, body, outgoingHeaders2, serverResponse);
606
+ }
607
+ const headers = new Headers(cachedHeaders);
608
+ if (typeof body === "string" && !headers.has("content-type")) {
609
+ headers.set("content-type", "text/plain;charset=UTF-8");
610
+ } else if (body instanceof Blob && body.type && !headers.has("content-type")) {
611
+ headers.set("content-type", body.type);
612
+ }
613
+ if (!headers.has("content-length")) {
614
+ if (typeof body === "string") {
615
+ headers.set("content-length", String(Buffer.byteLength(body)));
616
+ } else if (body instanceof Uint8Array) {
617
+ headers.set("content-length", String(body.byteLength));
618
+ } else if (body instanceof Blob) {
619
+ headers.set("content-length", String(body.size));
620
+ }
621
+ }
622
+ const outgoingHeaders = {};
623
+ const setCookies = headers.getSetCookie?.();
624
+ for (const [name, value] of headers) {
625
+ outgoingHeaders[name] = name === "set-cookie" && setCookies?.length ? setCookies : value;
626
+ }
627
+ return writeCachedBody(response, body, outgoingHeaders, serverResponse);
628
+ }
629
+ function writeCachedBody(response, body, outgoingHeaders, serverResponse) {
630
+ if (!serverResponse.headersSent) {
631
+ if (!isHttp2ServerResponse(serverResponse) && response.statusText) {
632
+ setHttp1StatusMessage(serverResponse, response.statusText);
633
+ }
634
+ serverResponse.writeHead(response.status, outgoingHeaders);
635
+ }
636
+ if (body === null) {
637
+ serverResponse.end();
638
+ } else if (typeof body === "string" || body instanceof Uint8Array) {
639
+ serverResponse.end(body);
640
+ } else if (body instanceof Blob) {
641
+ return body.arrayBuffer().then((buffer) => {
642
+ serverResponse.end(new Uint8Array(buffer));
643
+ });
644
+ } else {
645
+ return writeReadableStreamToWritable(body, serverResponse);
646
+ }
647
+ }
648
+ function writeReadableStreamToWritable(stream, writable) {
224
649
  if (stream.locked) {
225
650
  throw new TypeError("ReadableStream is locked.");
226
651
  }
227
652
  const reader = stream.getReader();
228
653
  const flushable = writable;
229
- const onAbort = () => {
230
- reader.cancel().catch(() => {
231
- });
232
- };
233
- writable.once("close", onAbort);
234
- writable.once("error", onAbort);
235
- try {
236
- while (!writable.destroyed) {
237
- const { done, value } = await reader.read();
654
+ return new Promise((resolve, reject) => {
655
+ let settled = false;
656
+ const cleanup = () => {
657
+ writable.removeListener("close", onAbort);
658
+ writable.removeListener("error", onAbort);
659
+ writable.removeListener("drain", read);
660
+ try {
661
+ reader.releaseLock();
662
+ } catch {
663
+ }
664
+ };
665
+ const finish = (error) => {
666
+ if (settled) return;
667
+ settled = true;
668
+ cleanup();
669
+ if (error !== void 0) reject(error);
670
+ else resolve();
671
+ };
672
+ const onAbort = () => {
673
+ reader.cancel().then(
674
+ () => finish(),
675
+ () => finish()
676
+ );
677
+ };
678
+ const handleError = (error) => {
679
+ try {
680
+ if (!writable.destroyed) {
681
+ writable.destroy();
682
+ }
683
+ } catch {
684
+ }
685
+ finish(error);
686
+ };
687
+ function read() {
688
+ if (writable.destroyed) {
689
+ onAbort();
690
+ return;
691
+ }
692
+ reader.read().then(flow).catch(handleError);
693
+ }
694
+ const flow = ({ done, value }) => {
238
695
  if (done) {
239
696
  if (!writable.destroyed) writable.end();
240
- break;
697
+ finish();
698
+ return;
241
699
  }
242
700
  if (writable.write(value) === false) {
243
- await new Promise((resolve) => {
244
- if (writable.destroyed) {
245
- resolve();
246
- } else {
247
- writable.once("drain", resolve);
248
- }
249
- });
701
+ writable.once("drain", read);
702
+ } else {
703
+ read();
250
704
  }
251
705
  if (typeof flushable.flush === "function") {
252
706
  flushable.flush();
253
707
  }
254
- }
255
- } catch (error) {
256
- if (!writable.destroyed) {
257
- writable.destroy(error instanceof Error ? error : void 0);
258
- throw error;
259
- }
260
- } finally {
261
- writable.removeListener("close", onAbort);
262
- writable.removeListener("error", onAbort);
263
- try {
264
- reader.releaseLock();
265
- } catch {
266
- }
267
- }
708
+ };
709
+ writable.once("close", onAbort);
710
+ writable.once("error", onAbort);
711
+ read();
712
+ });
268
713
  }
269
714
  function checkWritable(serverResponse) {
270
715
  if (serverResponse.writableEnded || serverResponse.finished) return false;
@@ -303,18 +748,119 @@ function buildToNodeHandler(dependencies2, options) {
303
748
  const env = process.env;
304
749
  const event = toFetchEvent(request);
305
750
  const maybePromise = webHandler(request, env, event);
306
- const respond = (response) => toServerResponse(response, serverResponse).finally(
307
- () => cancelRequestBody(request)
751
+ if (maybePromise instanceof Promise) {
752
+ maybePromise.then(
753
+ (response) => {
754
+ try {
755
+ const writing = toServerResponse(response, serverResponse);
756
+ if (writing instanceof Promise) {
757
+ writing.then(
758
+ () => cancelRequestBody(request),
759
+ () => {
760
+ cancelRequestBody(request);
761
+ sendServerError(serverResponse);
762
+ }
763
+ );
764
+ } else {
765
+ cancelRequestBody(request);
766
+ }
767
+ } catch {
768
+ cancelRequestBody(request);
769
+ sendServerError(serverResponse);
770
+ }
771
+ },
772
+ () => {
773
+ cancelRequestBody(request);
774
+ sendServerError(serverResponse);
775
+ }
776
+ );
777
+ return;
778
+ }
779
+ try {
780
+ const writing = toServerResponse(maybePromise, serverResponse);
781
+ if (writing instanceof Promise) {
782
+ writing.then(
783
+ () => cancelRequestBody(request),
784
+ () => {
785
+ cancelRequestBody(request);
786
+ sendServerError(serverResponse);
787
+ }
788
+ );
789
+ } else {
790
+ cancelRequestBody(request);
791
+ }
792
+ } catch {
793
+ cancelRequestBody(request);
794
+ sendServerError(serverResponse);
795
+ }
796
+ };
797
+ };
798
+ }
799
+ function buildToNodeSourceHandler(dependencies2, options) {
800
+ const toRequestSource = buildToRequestSource(dependencies2, options);
801
+ return function toNodeHandler(webHandler) {
802
+ return (incomingMessage, serverResponse) => {
803
+ const source = toRequestSource(incomingMessage);
804
+ const needsSourceCleanup = source.method !== "GET" && source.method !== "HEAD";
805
+ const maybePromise = webHandler(
806
+ source,
807
+ process.env,
808
+ NODE_EXECUTION_CONTEXT
308
809
  );
309
- const fail = () => sendServerError(serverResponse);
310
810
  if (maybePromise instanceof Promise) {
311
- maybePromise.then(respond).catch(fail);
312
- } else {
313
- respond(maybePromise).catch(fail);
811
+ maybePromise.then(
812
+ (response) => {
813
+ try {
814
+ const writing = toServerResponse(response, serverResponse);
815
+ if (writing instanceof Promise) {
816
+ writing.then(
817
+ () => {
818
+ if (needsSourceCleanup) source.cancel();
819
+ },
820
+ () => {
821
+ if (needsSourceCleanup) source.cancel();
822
+ sendServerError(serverResponse);
823
+ }
824
+ );
825
+ } else if (needsSourceCleanup) {
826
+ source.cancel();
827
+ }
828
+ } catch {
829
+ if (needsSourceCleanup) source.cancel();
830
+ sendServerError(serverResponse);
831
+ }
832
+ },
833
+ () => {
834
+ if (needsSourceCleanup) source.cancel();
835
+ sendServerError(serverResponse);
836
+ }
837
+ );
838
+ return;
839
+ }
840
+ try {
841
+ const writing = toServerResponse(maybePromise, serverResponse);
842
+ if (writing instanceof Promise) {
843
+ writing.then(
844
+ () => {
845
+ if (needsSourceCleanup) source.cancel();
846
+ },
847
+ () => {
848
+ if (needsSourceCleanup) source.cancel();
849
+ sendServerError(serverResponse);
850
+ }
851
+ );
852
+ } else if (needsSourceCleanup) {
853
+ source.cancel();
854
+ }
855
+ } catch {
856
+ if (needsSourceCleanup) source.cancel();
857
+ sendServerError(serverResponse);
314
858
  }
315
859
  };
316
860
  };
317
861
  }
318
862
  export {
319
- NodeAdapter as default
863
+ buildToNodeHandler,
864
+ NodeAdapter as default,
865
+ toServerResponse
320
866
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@web-widget/node",
3
3
  "description": "Node.js adapter",
4
- "version": "3.0.0-beta.2",
4
+ "version": "3.0.0-beta.5",
5
5
  "homepage": "https://github.com/web-widget/web-widget#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -13,13 +13,13 @@
13
13
  },
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
- "@edge-runtime/node-utils": "^4.0.0",
17
16
  "@edge-runtime/primitives": "^6.0.0"
18
17
  },
19
18
  "devDependencies": {
20
- "@types/node": "^22.2.0",
21
- "tsup": "^8.3.4",
22
- "typescript": "^5.9.3"
19
+ "@types/node": "catalog:",
20
+ "tsup": "catalog:",
21
+ "typescript": "catalog:",
22
+ "vitest": "catalog:"
23
23
  },
24
24
  "engines": {
25
25
  "node": ">=18"
@@ -27,6 +27,7 @@
27
27
  "exports": {
28
28
  ".": {
29
29
  "types": "./dist/node.d.ts",
30
+ "development": "./src/index.ts",
30
31
  "default": "./dist/node.js"
31
32
  }
32
33
  },
@@ -34,15 +35,23 @@
34
35
  "dist/"
35
36
  ],
36
37
  "publishConfig": {
37
- "access": "public"
38
+ "access": "public",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/node.d.ts",
42
+ "default": "./dist/node.js"
43
+ }
44
+ }
38
45
  },
39
- "type": "module",
40
46
  "scripts": {
41
47
  "build": "pnpm exec tsup",
42
48
  "clean:build": "rm -rf dist",
43
49
  "clean:node": "rm -rf node_modules",
44
50
  "dev": "pnpm exec tsup --watch",
45
51
  "lint": "eslint src",
46
- "prebuild": "pnpm run clean:build"
47
- }
48
- }
52
+ "prebuild": "pnpm run clean:build",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest"
55
+ },
56
+ "type": "module"
57
+ }