srvx 0.2.5 → 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,6 +1,7 @@
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';
@@ -13,8 +14,8 @@ type NodeFastResponse = InstanceType<typeof NodeFastResponse>;
13
14
  */
14
15
  declare const NodeFastResponse: {
15
16
  new (body?: BodyInit | null, init?: ResponseInit): {
16
- "__#4201@#body"?: BodyInit | null;
17
- "__#4201@#init"?: ResponseInit;
17
+ "__#4363@#body"?: BodyInit | null;
18
+ "__#4363@#init"?: ResponseInit;
18
19
  /**
19
20
  * Prepare Node.js response object
20
21
  */
@@ -25,11 +26,11 @@ declare const NodeFastResponse: {
25
26
  body: string | Uint8Array<ArrayBufferLike> | ReadableStream<Uint8Array<ArrayBufferLike>> | Readable | Buffer<ArrayBufferLike> | DataView<ArrayBufferLike> | null | undefined;
26
27
  };
27
28
  /** Lazy initialized response instance */
28
- "__#4201@#responseObj"?: Response;
29
+ "__#4363@#responseObj"?: Response;
29
30
  /** Lazy initialized headers instance */
30
- "__#4201@#headersObj"?: Headers;
31
+ "__#4363@#headersObj"?: Headers;
31
32
  clone(): Response;
32
- readonly "__#4201@#response": Response;
33
+ readonly "__#4363@#response": Response;
33
34
  readonly headers: Headers;
34
35
  readonly ok: boolean;
35
36
  readonly redirected: boolean;
@@ -37,7 +38,7 @@ declare const NodeFastResponse: {
37
38
  readonly statusText: string;
38
39
  readonly type: ResponseType;
39
40
  readonly url: string;
40
- "__#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;
41
42
  readonly body: ReadableStream<Uint8Array> | null;
42
43
  readonly bodyUsed: boolean;
43
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() {
@@ -761,13 +768,18 @@ class NodeServer {
761
768
  const res = fetchHandler(request);
762
769
  return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res);
763
770
  };
771
+ const tls = resolveTLSOptions(this.options);
764
772
  this.serveOptions = {
765
773
  port: resolvePort(this.options.port, globalThis.process?.env.PORT),
766
774
  host: this.options.hostname,
767
775
  exclusive: !this.options.reusePort,
776
+ ...tls ? { cert: tls.cert, key: tls.key, passphrase: tls.passphrase } : {},
768
777
  ...this.options.node
769
778
  };
770
- 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);
771
783
  this.node = { server, handler };
772
784
  if (!options.manual) {
773
785
  this.serve();
@@ -787,7 +799,11 @@ class NodeServer {
787
799
  if (!addr) {
788
800
  return;
789
801
  }
790
- 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
+ );
791
807
  }
792
808
  ready() {
793
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.5",
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 };