srvx 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  Universal Server API based on web platform standards. Works with [Deno](https://deno.com/), [Bun](https://bun.sh/) and [Node.js](https://nodejs.org/en).
11
11
 
12
- - ✅ Seamless runtime integration with identical usage ([handler](https://srvx.unjs.io/guide/handler) and
12
+ - ✅ Seamless runtime integration with identical usage ([handler](https://srvx.unjs.io/guide/handler) and [instance](https://srvx.unjs.io/guide/server))
13
13
  - ✅ Zero overhead [Deno](https://deno.com/) and [Bun](https://bun.sh/) support
14
14
  - ✅ [Node.js compatibility](https://srvx.unjs.io/guide/node) with ~native perf and [fast response](https://srvx.unjs.io/guide/node#fast-response) support
15
15
 
@@ -1,6 +1,7 @@
1
1
  import { ServerOptions, Server, BunFetchandler } from '../types.mjs';
2
2
  import * as Bun from 'bun';
3
3
  import 'node:http';
4
+ import 'node:https';
4
5
  import 'node:net';
5
6
  import '@cloudflare/workers-types';
6
7
 
@@ -16,7 +17,7 @@ declare class BunServer implements Server<BunFetchandler> {
16
17
  readonly runtime = "bun";
17
18
  readonly options: ServerOptions;
18
19
  readonly bun: Server["bun"];
19
- readonly serveOptions: Bun.ServeOptions;
20
+ readonly serveOptions: Bun.ServeOptions | Bun.TLSServeOptions;
20
21
  readonly fetch: BunFetchandler;
21
22
  constructor(options: ServerOptions);
22
23
  serve(): Promise<Awaited<this>>;
@@ -1,5 +1,6 @@
1
- import { r as resolvePort } from '../shared/srvx.PbkQy9Ck.mjs';
1
+ import { r as resolveTLSOptions, a as resolvePort } from '../shared/srvx.lC_d9z2b.mjs';
2
2
  import { w as wrapFetch } from '../shared/srvx.DhN4g5wJ.mjs';
3
+ import 'node:fs';
3
4
 
4
5
  const Response = globalThis.Response;
5
6
  function serve(options) {
@@ -21,11 +22,18 @@ class BunServer {
21
22
  });
22
23
  return fetchHandler(request);
23
24
  };
25
+ const tls = resolveTLSOptions(this.options);
24
26
  this.serveOptions = {
25
27
  hostname: this.options.hostname,
26
28
  reusePort: this.options.reusePort,
27
29
  port: resolvePort(this.options.port, globalThis.process?.env.PORT),
28
30
  ...this.options.bun,
31
+ tls: {
32
+ cert: tls?.cert,
33
+ key: tls?.key,
34
+ passphrase: tls?.passphrase,
35
+ ...this.options.bun?.tls
36
+ },
29
37
  fetch: this.fetch
30
38
  };
31
39
  if (!options.manual) {
@@ -1,6 +1,7 @@
1
1
  import { ServerOptions, Server } from '../types.mjs';
2
2
  import * as CF from '@cloudflare/workers-types';
3
3
  import 'node:http';
4
+ import 'node:https';
4
5
  import 'node:net';
5
6
  import 'bun';
6
7
 
@@ -1,5 +1,6 @@
1
1
  import { ServerOptions, Server, DenoFetchHandler } from '../types.mjs';
2
2
  import 'node:http';
3
+ import 'node:https';
3
4
  import 'node:net';
4
5
  import 'bun';
5
6
  import '@cloudflare/workers-types';
@@ -17,7 +18,7 @@ declare class DenoServer implements Server<DenoFetchHandler> {
17
18
  readonly runtime = "deno";
18
19
  readonly options: ServerOptions;
19
20
  readonly deno: Server["deno"];
20
- readonly serveOptions: Deno.ServeTcpOptions;
21
+ readonly serveOptions: Deno.ServeTcpOptions | (Deno.ServeTcpOptions & Deno.TlsCertifiedKeyPem);
21
22
  readonly fetch: DenoFetchHandler;
22
23
  constructor(options: ServerOptions);
23
24
  serve(): Promise<this>;
@@ -1,5 +1,6 @@
1
- import { r as resolvePort, f as fmtURL } from '../shared/srvx.PbkQy9Ck.mjs';
1
+ import { r as resolveTLSOptions, a as resolvePort, f as fmtURL } from '../shared/srvx.lC_d9z2b.mjs';
2
2
  import { w as wrapFetch } from '../shared/srvx.DhN4g5wJ.mjs';
3
+ import 'node:fs';
3
4
 
4
5
  const Response = globalThis.Response;
5
6
  function serve(options) {
@@ -21,10 +22,12 @@ class DenoServer {
21
22
  });
22
23
  return fetchHandler(request);
23
24
  };
25
+ const tls = resolveTLSOptions(this.options);
24
26
  this.serveOptions = {
25
27
  port: resolvePort(this.options.port, globalThis.Deno?.env.get("PORT")),
26
28
  hostname: this.options.hostname,
27
29
  reusePort: this.options.reusePort,
30
+ ...tls ? { key: tls.key, cert: tls.cert, passphrase: tls.passphrase } : {},
28
31
  ...this.options.deno
29
32
  };
30
33
  if (!options.manual) {
@@ -55,7 +58,11 @@ class DenoServer {
55
58
  return Promise.resolve(this.#listeningPromise).then(() => this);
56
59
  }
57
60
  get url() {
58
- return this.#listeningInfo ? fmtURL(this.#listeningInfo.hostname, this.#listeningInfo.port) : void 0;
61
+ return this.#listeningInfo ? fmtURL(
62
+ this.#listeningInfo.hostname,
63
+ this.#listeningInfo.port,
64
+ !!this.serveOptions.cert
65
+ ) : void 0;
59
66
  }
60
67
  ready() {
61
68
  return Promise.resolve(this.#listeningPromise).then(() => this);
@@ -1,15 +1,21 @@
1
1
  import { ServerOptions, Server, FetchHandler, NodeHttpHandler } from '../types.mjs';
2
2
  import NodeHttp__default from 'node:http';
3
3
  import { Readable } from 'node:stream';
4
+ import 'node:https';
4
5
  import 'node:net';
5
6
  import 'bun';
6
7
  import '@cloudflare/workers-types';
7
8
 
8
9
  type NodeFastResponse = InstanceType<typeof NodeFastResponse>;
10
+ /**
11
+ * Fast Response for Node.js runtime
12
+ *
13
+ * It is faster because in most cases it doesn't create a full Response instance.
14
+ */
9
15
  declare const NodeFastResponse: {
10
16
  new (body?: BodyInit | null, init?: ResponseInit): {
11
- "__#4201@#body"?: BodyInit | null;
12
- "__#4201@#init"?: ResponseInit;
17
+ "__#4363@#body"?: BodyInit | null;
18
+ "__#4363@#init"?: ResponseInit;
13
19
  /**
14
20
  * Prepare Node.js response object
15
21
  */
@@ -20,11 +26,11 @@ declare const NodeFastResponse: {
20
26
  body: string | Uint8Array<ArrayBufferLike> | ReadableStream<Uint8Array<ArrayBufferLike>> | Readable | Buffer<ArrayBufferLike> | DataView<ArrayBufferLike> | null | undefined;
21
27
  };
22
28
  /** Lazy initialized response instance */
23
- "__#4201@#responseObj"?: Response;
29
+ "__#4363@#responseObj"?: Response;
24
30
  /** Lazy initialized headers instance */
25
- "__#4201@#headersObj"?: Headers;
31
+ "__#4363@#headersObj"?: Headers;
26
32
  clone(): Response;
27
- readonly "__#4201@#response": Response;
33
+ readonly "__#4363@#response": Response;
28
34
  readonly headers: Headers;
29
35
  readonly ok: boolean;
30
36
  readonly redirected: boolean;
@@ -32,7 +38,7 @@ declare const NodeFastResponse: {
32
38
  readonly statusText: string;
33
39
  readonly type: ResponseType;
34
40
  readonly url: string;
35
- "__#4201@#fastBody"<T extends object>(as: new (...args: any[]) => T): T | null | false;
41
+ "__#4363@#fastBody"<T extends object>(as: new (...args: any[]) => T): T | null | false;
36
42
  readonly body: ReadableStream<Uint8Array> | null;
37
43
  readonly bodyUsed: boolean;
38
44
  arrayBuffer(): Promise<ArrayBuffer>;
@@ -1,7 +1,9 @@
1
1
  import NodeHttp from 'node:http';
2
+ import NodeHttps from 'node:https';
2
3
  import { splitSetCookieString } from 'cookie-es';
3
- import { r as resolvePort, f as fmtURL } from '../shared/srvx.PbkQy9Ck.mjs';
4
+ import { r as resolveTLSOptions, a as resolvePort, f as fmtURL } from '../shared/srvx.lC_d9z2b.mjs';
4
5
  import { w as wrapFetch } from '../shared/srvx.DhN4g5wJ.mjs';
6
+ import 'node:fs';
5
7
 
6
8
  async function sendNodeResponse(nodeRes, webRes) {
7
9
  if (!webRes) {
@@ -11,7 +13,7 @@ async function sendNodeResponse(nodeRes, webRes) {
11
13
  if (webRes.nodeResponse) {
12
14
  const res = webRes.nodeResponse();
13
15
  if (!nodeRes.headersSent) {
14
- nodeRes.writeHead(res.status, res.statusText, res.headers);
16
+ nodeRes.writeHead(res.status, res.statusText, res.headers.flat());
15
17
  }
16
18
  if (res.body) {
17
19
  if (res.body instanceof ReadableStream) {
@@ -35,7 +37,11 @@ async function sendNodeResponse(nodeRes, webRes) {
35
37
  }
36
38
  }
37
39
  if (!nodeRes.headersSent) {
38
- nodeRes.writeHead(webRes.status || 200, webRes.statusText, headerEntries);
40
+ nodeRes.writeHead(
41
+ webRes.status || 200,
42
+ webRes.statusText,
43
+ headerEntries.flat()
44
+ );
39
45
  }
40
46
  return webRes.body ? streamBody(webRes.body, nodeRes) : endNodeResponse(nodeRes);
41
47
  }
@@ -416,6 +422,7 @@ const NodeRequestProxy = /* @__PURE__ */ (() => {
416
422
  return false;
417
423
  }
418
424
  }
425
+ this.#hasBody = true;
419
426
  return true;
420
427
  }
421
428
  get body() {
@@ -516,13 +523,8 @@ async function _readStream(stream) {
516
523
  return Buffer.concat(chunks);
517
524
  }
518
525
 
519
- const NodeFastResponse = /* @__PURE__ */ (() => (
520
- /**
521
- * Fast Response for Node.js runtime
522
- *
523
- * It is faster because in most cases it doesn't create a full Response instance.
524
- */
525
- class NodeFastResponse {
526
+ const NodeFastResponse = /* @__PURE__ */ (() => {
527
+ class NodeFastResponse2 {
526
528
  #body;
527
529
  #init;
528
530
  constructor(body, init) {
@@ -739,7 +741,9 @@ const NodeFastResponse = /* @__PURE__ */ (() => (
739
741
  return this.text().then((text) => JSON.parse(text));
740
742
  }
741
743
  }
742
- ))();
744
+ Object.setPrototypeOf(NodeFastResponse2.prototype, Response.prototype);
745
+ return NodeFastResponse2;
746
+ })();
743
747
 
744
748
  function serve(options) {
745
749
  return new NodeServer(options);
@@ -764,13 +768,18 @@ class NodeServer {
764
768
  const res = fetchHandler(request);
765
769
  return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res);
766
770
  };
771
+ const tls = resolveTLSOptions(this.options);
767
772
  this.serveOptions = {
768
773
  port: resolvePort(this.options.port, globalThis.process?.env.PORT),
769
774
  host: this.options.hostname,
770
775
  exclusive: !this.options.reusePort,
776
+ ...tls ? { cert: tls.cert, key: tls.key, passphrase: tls.passphrase } : {},
771
777
  ...this.options.node
772
778
  };
773
- const server = NodeHttp.createServer(this.serveOptions, handler);
779
+ const server = this.serveOptions.cert ? NodeHttps.createServer(
780
+ this.serveOptions,
781
+ handler
782
+ ) : NodeHttp.createServer(this.serveOptions, handler);
774
783
  this.node = { server, handler };
775
784
  if (!options.manual) {
776
785
  this.serve();
@@ -790,7 +799,11 @@ class NodeServer {
790
799
  if (!addr) {
791
800
  return;
792
801
  }
793
- return typeof addr === "string" ? addr : fmtURL(addr.address, addr.port);
802
+ return typeof addr === "string" ? addr : fmtURL(
803
+ addr.address,
804
+ addr.port,
805
+ this.node.server instanceof NodeHttps.Server
806
+ );
794
807
  }
795
808
  ready() {
796
809
  return Promise.resolve(this.#listeningPromise).then(() => this);
@@ -0,0 +1,57 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ function resolvePort(portOptions, portEnv) {
4
+ const portInput = portOptions ?? portEnv;
5
+ if (portInput === void 0) {
6
+ return 3e3;
7
+ }
8
+ return typeof portInput === "number" ? portInput : Number.parseInt(portInput, 10);
9
+ }
10
+ function fmtURL(host, port, secure) {
11
+ if (!host || !port) {
12
+ return void 0;
13
+ }
14
+ if (host.includes(":")) {
15
+ host = `[${host}]`;
16
+ }
17
+ return `http${secure ? "s" : ""}://${host}:${port}/`;
18
+ }
19
+ function resolveTLSOptions(opts) {
20
+ if (!opts.tls || opts.protocol === "http") {
21
+ return;
22
+ }
23
+ const cert = resolveCertOrKey(opts.tls.cert);
24
+ const key = resolveCertOrKey(opts.tls.key);
25
+ if (!cert && !key) {
26
+ if (opts.protocol === "https") {
27
+ throw new TypeError(
28
+ "TLS `cert` and `key` must be provided for `https` protocol."
29
+ );
30
+ }
31
+ return;
32
+ }
33
+ if (!cert || !key) {
34
+ throw new TypeError("TLS `cert` and `key` must be provided together.");
35
+ }
36
+ return {
37
+ cert,
38
+ key,
39
+ passphrase: opts.tls.passphrase
40
+ };
41
+ }
42
+ function resolveCertOrKey(value) {
43
+ if (!value) {
44
+ return;
45
+ }
46
+ if (typeof value !== "string") {
47
+ throw new TypeError(
48
+ "TLS certificate and key must be strings in PEM format or file paths."
49
+ );
50
+ }
51
+ if (value.startsWith("-----BEGIN ")) {
52
+ return value;
53
+ }
54
+ return readFileSync(value, "utf8");
55
+ }
56
+
57
+ export { resolvePort as a, fmtURL as f, resolveTLSOptions as r };
package/dist/types.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as NodeHttp from 'node:http';
2
+ import * as NodeHttps from 'node:https';
2
3
  import * as NodeNet from 'node:net';
3
4
  import * as Bun from 'bun';
4
5
  import * as CF from '@cloudflare/workers-types';
@@ -51,16 +52,41 @@ interface ServerOptions {
51
52
  * **Note:** Despite Node.js built-in behavior that has `exclusive` flag (opposite of `reusePort`) enabled by default, srvx uses non-exclusive mode for consistency.
52
53
  */
53
54
  reusePort?: boolean;
55
+ /**
56
+ * The protocol to use for the server.
57
+ *
58
+ * Possible values are `http` and `https`.
59
+ *
60
+ * If `protocol` is not set, Server will use `http` as the default protocol or `https` if both `tls.cert` and `tls.key` options are provided.
61
+ */
62
+ protocol?: "http" | "https";
63
+ /**
64
+ * TLS server options.
65
+ */
66
+ tls?: {
67
+ /**
68
+ * File path or inlined TLS certificate in PEM format (required).
69
+ */
70
+ cert?: string;
71
+ /**
72
+ * File path or inlined TLS private key in PEM format (required).
73
+ */
74
+ key?: string;
75
+ /**
76
+ * Passphrase for the private key (optional).
77
+ */
78
+ passphrase?: string;
79
+ };
54
80
  /**
55
81
  * Node.js server options.
56
82
  */
57
- node?: NodeHttp.ServerOptions & NodeNet.ListenOptions;
83
+ node?: (NodeHttp.ServerOptions | NodeHttps.ServerOptions) & NodeNet.ListenOptions;
58
84
  /**
59
85
  * Bun server options
60
86
  *
61
87
  * @docs https://bun.sh/docs/api/http
62
88
  */
63
- bun?: Omit<Bun.ServeOptions, "fetch">;
89
+ bun?: Omit<Bun.ServeOptions | Bun.TLSServeOptions, "fetch">;
64
90
  /**
65
91
  * Deno server options
66
92
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "srvx",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Universal Server API based on web platform standards. Works seamlessly with Deno, Bun and Node.js.",
5
5
  "repository": "unjs/srvx",
6
6
  "license": "MIT",
@@ -30,6 +30,7 @@
30
30
  "dev": "vitest dev",
31
31
  "lint": "eslint . && prettier -c .",
32
32
  "lint:fix": "automd && eslint . --fix && prettier -w .",
33
+ "play:mkcert": "openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj /CN=srvx.local",
33
34
  "play:node": "node playground/app.mjs",
34
35
  "play:deno": "deno run -A playground/app.mjs",
35
36
  "play:bun": "bun playground/app.mjs",
@@ -46,16 +47,16 @@
46
47
  "cookie-es": "^2.0.0"
47
48
  },
48
49
  "devDependencies": {
49
- "@cloudflare/workers-types": "^4.20250303.0",
50
- "@hono/node-server": "^1.13.8",
50
+ "@cloudflare/workers-types": "^4.20250321.0",
51
+ "@hono/node-server": "^1.14.0",
51
52
  "@mjackson/node-fetch-server": "^0.6.1",
52
- "@types/bun": "^1.2.4",
53
+ "@types/bun": "^1.2.5",
53
54
  "@types/deno": "^2.2.0",
54
- "@types/node": "^22.13.10",
55
- "@vitest/coverage-v8": "^3.0.8",
55
+ "@types/node": "^22.13.13",
56
+ "@vitest/coverage-v8": "^3.0.9",
56
57
  "automd": "^0.4.0",
57
58
  "changelogen": "^0.6.1",
58
- "eslint": "^9.22.0",
59
+ "eslint": "^9.23.0",
59
60
  "eslint-config-unjs": "^0.4.2",
60
61
  "execa": "^9.5.2",
61
62
  "get-port-please": "^3.1.2",
@@ -63,7 +64,7 @@
63
64
  "prettier": "^3.5.3",
64
65
  "typescript": "^5.8.2",
65
66
  "unbuild": "^3.5.0",
66
- "vitest": "^3.0.8"
67
+ "vitest": "^3.0.9"
67
68
  },
68
- "packageManager": "pnpm@10.6.2"
69
+ "packageManager": "pnpm@10.6.5"
69
70
  }
@@ -1,18 +0,0 @@
1
- function resolvePort(portOptions, portEnv) {
2
- const portInput = portOptions ?? portEnv;
3
- if (portInput === void 0) {
4
- return 3e3;
5
- }
6
- return typeof portInput === "number" ? portInput : Number.parseInt(portInput, 10);
7
- }
8
- function fmtURL(host, port, ssl) {
9
- if (!host || !port) {
10
- return void 0;
11
- }
12
- if (host.includes(":")) {
13
- host = `[${host}]`;
14
- }
15
- return `http${""}://${host}:${port}/`;
16
- }
17
-
18
- export { fmtURL as f, resolvePort as r };