srvx 0.8.2 → 0.8.3

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
@@ -1,47 +1,74 @@
1
- # 💥 srvx
1
+ # λ srvx
2
2
 
3
- <!-- automd:badges color=yellow -->
3
+ <!-- automd:badges color=yellow packagephobia -->
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/srvx?color=yellow)](https://npmjs.com/package/srvx)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/srvx?color=yellow)](https://npm.chart.dev/srvx)
7
+ [![install size](https://badgen.net/packagephobia/install/srvx?color=yellow)](https://packagephobia.com/result?p=srvx)
7
8
 
8
9
  <!-- /automd -->
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
+ Universal Server based on web standards. Works with [Deno](https://deno.com/), [Bun](https://bun.sh/) and [Node.js](https://nodejs.org/en).
11
12
 
12
- - ✅ Seamless runtime integration with identical usage ([handler](https://srvx.h3.dev/guide/handler) and [instance](https://srvx.h3.dev/guide/server))
13
- - ✅ Zero overhead [Deno](https://deno.com/) and [Bun](https://bun.sh/) support
14
- - ✅ [Node.js compatibility](https://srvx.h3.dev/guide/node) with ~native perf and [fast response](https://srvx.h3.dev/guide/node#fast-response) support
13
+ - ✅ Zero dependency
14
+ - ✅ Full featured CLI with watcher, error handler, serve static and logger
15
+ - ✅ Seamless runtime integration with same API ([handler](https://srvx.h3.dev/guide/handler) and [instance](https://srvx.h3.dev/guide/server)).
16
+ - ✅ [Node.js compatibility](https://srvx.h3.dev/guide/node) with up to [~96.98%](https://github.com/h3js/srvx/tree/main/test/bench-node) native performance.
17
+ - ✅ Zero overhead [Deno](https://deno.com/) and [Bun](https://bun.sh/) support.
15
18
 
16
19
  ## Quick start
17
20
 
18
21
  ```js
19
- import { serve } from "srvx";
20
-
21
- const server = serve({
22
- port: 3000,
23
- fetch(request) {
24
- return new Response("👋 Hello there!");
22
+ export default {
23
+ fetch(req: Request) {
24
+ return Response.json({ hello: "world!" });
25
25
  },
26
- });
26
+ };
27
+ ```
28
+
29
+ Then, run the server using your favorite runtime:
30
+
31
+ ```bash
32
+ # Node.js
33
+ $ npx srvx # npm
34
+ $ pnpx srvx # pnpm
35
+ $ yarn dlx srvx # yarn
36
+
37
+ # Deno
38
+ $ deno -A npm:srvx
39
+
40
+ # Bun
41
+ $ bunx --bun srvx
27
42
  ```
28
43
 
29
44
  👉 **Visit the 📖 [Documentation](https://srvx.h3.dev/) to learn more.**
30
45
 
31
- ## Development
46
+ ## Starter Examples
32
47
 
33
- <details>
48
+ [➤ Online Playground](https://stackblitz.com/fork/github/h3js/srvx/tree/main/examples/stackblitz?startScript=dev&file=server.mjs)
34
49
 
35
- <summary>local development</summary>
50
+ <!-- automd:examples -->
51
+
52
+ | Example | Source | Try |
53
+ | ---------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
54
+ | `elysia` | [examples/elysia](https://github.com/h3js/srvx/tree/main/examples/elysia/) | `npx giget gh:h3js/srvx/examples/elysia srvx-elysia` |
55
+ | `h3` | [examples/h3](https://github.com/h3js/srvx/tree/main/examples/h3/) | `npx giget gh:h3js/srvx/examples/h3 srvx-h3` |
56
+ | `hello-world` | [examples/hello-world](https://github.com/h3js/srvx/tree/main/examples/hello-world/) | `npx giget gh:h3js/srvx/examples/hello-world srvx-hello-world` |
57
+ | `hono` | [examples/hono](https://github.com/h3js/srvx/tree/main/examples/hono/) | `npx giget gh:h3js/srvx/examples/hono srvx-hono` |
58
+ | `service-worker` | [examples/service-worker](https://github.com/h3js/srvx/tree/main/examples/service-worker/) | `npx giget gh:h3js/srvx/examples/service-worker srvx-service-worker` |
59
+ | `websocket` | [examples/websocket](https://github.com/h3js/srvx/tree/main/examples/websocket/) | `npx giget gh:h3js/srvx/examples/websocket srvx-websocket` |
60
+
61
+ <!-- /automd -->
62
+
63
+ ## Contribution
36
64
 
37
65
  - Clone this repository
38
66
  - Install the latest LTS version of [Node.js](https://nodejs.org/en/)
39
67
  - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
40
68
  - Install dependencies using `pnpm install`
69
+ - **Prepare stub mode using `pnpm build --stub`**
41
70
  - Run interactive tests using `pnpm dev`
42
71
 
43
- </details>
44
-
45
72
  ## License
46
73
 
47
74
  <!-- automd:contributors author=pi0 license=MIT -->
package/bin/srvx.mjs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../dist/cli.mjs";
3
+
4
+ await main({
5
+ command: "srvx",
6
+ docs: "https://srvx.h3.dev",
7
+ issues: "https://github.com/h3js/srvx/issues",
8
+ });
@@ -28,7 +28,7 @@ function printListening(opts, url) {
28
28
  url = `\u001B[36m${url}\u001B[0m`;
29
29
  additionalInfo = `\u001B[2m${additionalInfo}\u001B[0m`;
30
30
  }
31
- console.log(` ${listeningOn} ${url}${additionalInfo}`);
31
+ console.log(`${listeningOn} ${url}${additionalInfo}`);
32
32
  }
33
33
  function resolveTLSOptions(opts) {
34
34
  if (!opts.tls || opts.protocol === "http") return;
@@ -0,0 +1,17 @@
1
+ //#region src/_utils.cli.ts
2
+ const noColor = globalThis.process?.env?.NO_COLOR === "1" || globalThis.process?.env?.TERM === "dumb";
3
+ const _c = (c) => (t) => noColor ? t : `\u001B[${c}m${t}\u001B[0m`;
4
+ const Colors = {
5
+ bold: _c(1),
6
+ red: _c(31),
7
+ green: _c(32),
8
+ yellow: _c(33),
9
+ blue: _c(34),
10
+ magenta: _c(35),
11
+ cyan: _c(36),
12
+ gray: _c(90),
13
+ url: (title, url) => noColor ? `${title} (${url})` : `\u001B]8;;${url}\u001B\\${title}\u001B]8;;\u001B\\`
14
+ };
15
+
16
+ //#endregion
17
+ export { Colors };
@@ -0,0 +1,104 @@
1
+ //#region package.json
2
+ var name = "srvx";
3
+ var version = "0.8.3";
4
+ var description = "Universal Server API based on web platform standards. Works seamlessly with Deno, Bun and Node.js.";
5
+ var homepage = "https://srvx.h3.dev";
6
+ var repository = "h3js/srvx";
7
+ var license = "MIT";
8
+ var sideEffects = false;
9
+ var type = "module";
10
+ var exports = {
11
+ "./deno": "./dist/adapters/deno.mjs",
12
+ "./bun": "./dist/adapters/bun.mjs",
13
+ "./node": "./dist/adapters/node.mjs",
14
+ "./cloudflare": "./dist/adapters/cloudflare.mjs",
15
+ "./generic": "./dist/adapters/generic.mjs",
16
+ "./service-worker": "./dist/adapters/service-worker.mjs",
17
+ "./cli": "./dist/cli.mjs",
18
+ "./static": "./dist/static.mjs",
19
+ "./log": "./dist/log.mjs",
20
+ ".": {
21
+ "types": "./dist/types.d.mts",
22
+ "deno": "./dist/adapters/deno.mjs",
23
+ "bun": "./dist/adapters/bun.mjs",
24
+ "workerd": "./dist/adapters/cloudflare.mjs",
25
+ "browser": "./dist/adapters/service-worker.mjs",
26
+ "node": "./dist/adapters/node.mjs",
27
+ "default": "./dist/adapters/generic.mjs"
28
+ }
29
+ };
30
+ var types = "./dist/types.d.mts";
31
+ var bin = { "srvx": "./bin/srvx.mjs" };
32
+ var files = ["bin", "dist"];
33
+ var scripts = {
34
+ "bench:node": "node test/bench-node/_run.mjs",
35
+ "bench:url:bun": "bun run ./test/url.bench.ts",
36
+ "bench:url:deno": "deno run -A ./test/url.bench.ts",
37
+ "bench:url:node": "pnpm node-ts --expose-gc --allow-natives-syntax ./test/url.bench.ts",
38
+ "build": "obuild",
39
+ "dev": "vitest dev",
40
+ "lint": "eslint . && prettier -c .",
41
+ "lint:fix": "automd && eslint . --fix && prettier -w .",
42
+ "node-ts": "node --disable-warning=ExperimentalWarning --experimental-strip-types",
43
+ "prepack": "pnpm build",
44
+ "play:mkcert": "openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj /CN=srvx.local",
45
+ "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags",
46
+ "srvx": "./bin/srvx.mjs",
47
+ "test": "pnpm lint && pnpm test:types && vitest run --coverage",
48
+ "test:types": "tsc --noEmit --skipLibCheck"
49
+ };
50
+ var resolutions = { "srvx": "link:." };
51
+ var dependencies = { "cookie-es": "^2.0.0" };
52
+ var devDependencies = {
53
+ "@cloudflare/workers-types": "^4.20250627.0",
54
+ "@hono/node-server": "^1.14.4",
55
+ "@mitata/counters": "^0.0.8",
56
+ "@mjackson/node-fetch-server": "^0.7.0",
57
+ "@types/bun": "^1.2.17",
58
+ "@types/deno": "^2.3.0",
59
+ "@types/node": "^24.0.4",
60
+ "@types/node-forge": "^1.3.11",
61
+ "@types/serviceworker": "^0.0.139",
62
+ "@vitest/coverage-v8": "^3.2.4",
63
+ "@whatwg-node/server": "^0.10.10",
64
+ "automd": "^0.4.0",
65
+ "changelogen": "^0.6.1",
66
+ "eslint": "^9.29.0",
67
+ "eslint-config-unjs": "^0.5.0",
68
+ "execa": "^9.6.0",
69
+ "get-port-please": "^3.1.2",
70
+ "mdbox": "^0.1.1",
71
+ "mitata": "^1.0.34",
72
+ "node-forge": "^1.3.1",
73
+ "obuild": "^0.2.1",
74
+ "prettier": "^3.6.2",
75
+ "tslib": "^2.8.1",
76
+ "typescript": "^5.8.3",
77
+ "undici": "^7.11.0",
78
+ "vitest": "^3.2.4"
79
+ };
80
+ var packageManager = "pnpm@10.12.4";
81
+ var engines = { "node": ">=20.16.0" };
82
+ var package_default = {
83
+ name,
84
+ version,
85
+ description,
86
+ homepage,
87
+ repository,
88
+ license,
89
+ sideEffects,
90
+ type,
91
+ exports,
92
+ types,
93
+ bin,
94
+ files,
95
+ scripts,
96
+ resolutions,
97
+ dependencies,
98
+ devDependencies,
99
+ packageManager,
100
+ engines
101
+ };
102
+
103
+ //#endregion
104
+ export { package_default as default };
@@ -1,5 +1,5 @@
1
- import { BunFetchHandler, Server, ServerOptions } from "../_chunks/types-IGvUeyh_.mjs";
2
- import { FastURL$2 as FastURL } from "../_chunks/_url-CE8HuNzA.mjs";
1
+ import { BunFetchHandler, Server, ServerOptions } from "../_chunks/types-GwbH0R0m.mjs";
2
+ import { FastURL$2 as FastURL } from "../_chunks/_url-Bu46KnwC.mjs";
3
3
  import * as bun from "bun";
4
4
 
5
5
  //#region src/adapters/bun.d.ts
@@ -1,6 +1,6 @@
1
- import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils-ek-l5xdY.mjs";
2
- import { wrapFetch } from "../_chunks/_middleware-Bz7WmB2e.mjs";
3
- import { FastURL$1 as FastURL } from "../_chunks/_url-0eSmxIIk.mjs";
1
+ import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils-DRF_4b_y.mjs";
2
+ import { wrapFetch } from "../_chunks/_middleware-BvRR7B4M.mjs";
3
+ import { FastURL$1 as FastURL } from "../_chunks/_url-CdE4ce6F.mjs";
4
4
 
5
5
  //#region src/adapters/bun.ts
6
6
  const FastResponse = Response;
@@ -1,4 +1,4 @@
1
- import { Server, ServerOptions } from "../_chunks/types-IGvUeyh_.mjs";
1
+ import { Server, ServerOptions } from "../_chunks/types-GwbH0R0m.mjs";
2
2
  import * as CF from "@cloudflare/workers-types";
3
3
 
4
4
  //#region src/adapters/cloudflare.d.ts
@@ -1,5 +1,5 @@
1
- import { wrapFetch } from "../_chunks/_middleware-Bz7WmB2e.mjs";
2
- import { errorPlugin } from "../_chunks/_plugins-ta8pkwXd.mjs";
1
+ import { wrapFetch } from "../_chunks/_middleware-BvRR7B4M.mjs";
2
+ import { errorPlugin } from "../_chunks/_plugins-CbGQstxC.mjs";
3
3
 
4
4
  //#region src/adapters/cloudflare.ts
5
5
  const FastURL = URL;
@@ -1,5 +1,5 @@
1
- import { DenoFetchHandler, Server, ServerOptions } from "../_chunks/types-IGvUeyh_.mjs";
2
- import { FastURL$2 as FastURL } from "../_chunks/_url-CE8HuNzA.mjs";
1
+ import { DenoFetchHandler, Server, ServerOptions } from "../_chunks/types-GwbH0R0m.mjs";
2
+ import { FastURL$2 as FastURL } from "../_chunks/_url-Bu46KnwC.mjs";
3
3
 
4
4
  //#region src/adapters/deno.d.ts
5
5
  declare const FastResponse: typeof globalThis.Response;
@@ -1,6 +1,6 @@
1
- import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils-ek-l5xdY.mjs";
2
- import { wrapFetch } from "../_chunks/_middleware-Bz7WmB2e.mjs";
3
- import { FastURL$1 as FastURL } from "../_chunks/_url-0eSmxIIk.mjs";
1
+ import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils-DRF_4b_y.mjs";
2
+ import { wrapFetch } from "../_chunks/_middleware-BvRR7B4M.mjs";
3
+ import { FastURL$1 as FastURL } from "../_chunks/_url-CdE4ce6F.mjs";
4
4
 
5
5
  //#region src/adapters/deno.ts
6
6
  const FastResponse = Response;
@@ -1,4 +1,4 @@
1
- import { Server, ServerOptions } from "../_chunks/types-IGvUeyh_.mjs";
1
+ import { Server, ServerOptions } from "../_chunks/types-GwbH0R0m.mjs";
2
2
 
3
3
  //#region src/adapters/generic.d.ts
4
4
  declare const FastURL: typeof globalThis.URL;
@@ -1,6 +1,6 @@
1
- import { createWaitUntil } from "../_chunks/_utils-ek-l5xdY.mjs";
2
- import { wrapFetch } from "../_chunks/_middleware-Bz7WmB2e.mjs";
3
- import { errorPlugin } from "../_chunks/_plugins-ta8pkwXd.mjs";
1
+ import { createWaitUntil } from "../_chunks/_utils-DRF_4b_y.mjs";
2
+ import { wrapFetch } from "../_chunks/_middleware-BvRR7B4M.mjs";
3
+ import { errorPlugin } from "../_chunks/_plugins-CbGQstxC.mjs";
4
4
 
5
5
  //#region src/adapters/generic.ts
6
6
  const FastURL = URL;
@@ -1,5 +1,5 @@
1
- import { FetchHandler, NodeHttpHandler, NodeServerRequest, NodeServerResponse, Server, ServerOptions, ServerRequest } from "../_chunks/types-IGvUeyh_.mjs";
2
- import { FastURL$2 as FastURL } from "../_chunks/_url-CE8HuNzA.mjs";
1
+ import { FetchHandler, NodeHttpHandler, NodeServerRequest, NodeServerResponse, Server, ServerOptions, ServerRequest } from "../_chunks/types-GwbH0R0m.mjs";
2
+ import { FastURL$2 as FastURL } from "../_chunks/_url-Bu46KnwC.mjs";
3
3
  import NodeHttp from "node:http";
4
4
  import { Readable } from "node:stream";
5
5
 
@@ -1,7 +1,7 @@
1
- import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils-ek-l5xdY.mjs";
2
- import { wrapFetch } from "../_chunks/_middleware-Bz7WmB2e.mjs";
3
- import { FastURL$1 as FastURL } from "../_chunks/_url-0eSmxIIk.mjs";
4
- import { errorPlugin } from "../_chunks/_plugins-ta8pkwXd.mjs";
1
+ import { createWaitUntil, fmtURL, printListening, resolvePortAndHost, resolveTLSOptions } from "../_chunks/_utils-DRF_4b_y.mjs";
2
+ import { wrapFetch } from "../_chunks/_middleware-BvRR7B4M.mjs";
3
+ import { FastURL$1 as FastURL } from "../_chunks/_url-CdE4ce6F.mjs";
4
+ import { errorPlugin } from "../_chunks/_plugins-CbGQstxC.mjs";
5
5
  import { splitSetCookieString } from "cookie-es";
6
6
 
7
7
  //#region src/adapters/_node/send.ts
@@ -1,4 +1,4 @@
1
- import { Server, ServerOptions, ServerRequest } from "../_chunks/types-IGvUeyh_.mjs";
1
+ import { Server, ServerOptions, ServerRequest } from "../_chunks/types-GwbH0R0m.mjs";
2
2
 
3
3
  //#region src/adapters/service-worker.d.ts
4
4
  declare const FastURL: typeof globalThis.URL;
@@ -1,5 +1,5 @@
1
- import { wrapFetch } from "../_chunks/_middleware-Bz7WmB2e.mjs";
2
- import { errorPlugin } from "../_chunks/_plugins-ta8pkwXd.mjs";
1
+ import { wrapFetch } from "../_chunks/_middleware-BvRR7B4M.mjs";
2
+ import { errorPlugin } from "../_chunks/_plugins-CbGQstxC.mjs";
3
3
 
4
4
  //#region src/adapters/service-worker.ts
5
5
  const FastURL = URL;
package/dist/cli.d.mts ADDED
@@ -0,0 +1,10 @@
1
+ //#region src/cli.d.ts
2
+ declare function main(mainOpts: MainOpts): Promise<void>;
3
+ type MainOpts = {
4
+ command: string;
5
+ docs: string;
6
+ issues: string;
7
+ };
8
+ declare function usage(mainOpts: MainOpts): string;
9
+ //#endregion
10
+ export { main, usage };
package/dist/cli.mjs ADDED
@@ -0,0 +1,270 @@
1
+ import { Colors } from "./_chunks/_utils.cli-4eOGyiAa.mjs";
2
+ import { parseArgs } from "node:util";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { dirname, extname, relative, resolve } from "node:path";
5
+ import { fork } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+
8
+ //#region src/cli.ts
9
+ const defaultEntries = [
10
+ "server",
11
+ "src/server",
12
+ "index",
13
+ "src/index"
14
+ ];
15
+ const defaultExts = [
16
+ ".mts",
17
+ ".ts",
18
+ ".cts",
19
+ ".js",
20
+ ".mjs",
21
+ ".cjs"
22
+ ];
23
+ const args = process.argv.slice(2);
24
+ const options = parseArgs$1(args);
25
+ if (process.send) {
26
+ setupProcessErrorHandlers();
27
+ await serve();
28
+ }
29
+ async function main(mainOpts) {
30
+ setupProcessErrorHandlers();
31
+ if (options._version) {
32
+ console.log(await version());
33
+ process.exit(0);
34
+ }
35
+ if (options._help) {
36
+ console.log(usage(mainOpts));
37
+ process.exit(options._help ? 0 : 1);
38
+ }
39
+ if (options._prod) await serve();
40
+ else {
41
+ const isBun = !!process.versions.bun;
42
+ const isDeno = !!process.versions.deno;
43
+ const isNode = !isBun && !isDeno;
44
+ const runtimeArgs = ["--watch"];
45
+ if (isNode || isDeno) runtimeArgs.push(...[".env", ".env.local"].filter((f) => existsSync(f)).map((f) => `--env-file=${f}`));
46
+ const child = fork(fileURLToPath(import.meta.url), args, { execArgv: [...process.execArgv, ...runtimeArgs].filter(Boolean) });
47
+ child.on("error", (error) => {
48
+ console.error("Error in child process:", error);
49
+ process.exit(1);
50
+ });
51
+ child.on("exit", (code) => {
52
+ if (code !== 0) {
53
+ console.error(`Child process exited with code ${code}`);
54
+ process.exit(code);
55
+ }
56
+ });
57
+ }
58
+ }
59
+ async function serve() {
60
+ try {
61
+ const { serve: srvxServe } = await import("srvx");
62
+ const { serveStatic } = await import("srvx/static");
63
+ const { log } = await import("srvx/log");
64
+ const entry = await loadEntry(options);
65
+ const staticDir = resolve(options._dir, options._static);
66
+ options._static = existsSync(staticDir) ? staticDir : "";
67
+ const server = await srvxServe({
68
+ error: (error) => {
69
+ console.error(error);
70
+ return renderError(error);
71
+ },
72
+ middleware: [
73
+ log(),
74
+ options._static ? serveStatic({ dir: options._static }) : void 0,
75
+ ...entry.middleware || []
76
+ ].filter(Boolean),
77
+ ...entry
78
+ }).ready();
79
+ printInfo();
80
+ const cleanup = () => {
81
+ server.close(true).catch(() => {});
82
+ process.exit(0);
83
+ };
84
+ process.on("SIGINT", () => {
85
+ console.log(Colors.gray("\rStopping server..."));
86
+ cleanup();
87
+ });
88
+ process.on("SIGTERM", cleanup);
89
+ } catch (error) {
90
+ console.error(error);
91
+ process.exit(1);
92
+ }
93
+ }
94
+ async function loadEntry(opts) {
95
+ try {
96
+ if (!opts._entry) for (const entry of defaultEntries) {
97
+ for (const ext of defaultExts) {
98
+ const entryPath = resolve(opts._dir, `${entry}${ext}`);
99
+ if (existsSync(entryPath)) {
100
+ opts._entry = entryPath;
101
+ break;
102
+ }
103
+ }
104
+ if (opts._entry) break;
105
+ }
106
+ if (!opts._entry) return {
107
+ fetch: () => renderError(`No server entry file found.\nPlease specify an entry file or ensure one of the default entries exists (${defaultEntries.join(", ")}).`, 404, "No Server Entry"),
108
+ ...opts
109
+ };
110
+ const entryURL = opts._entry.startsWith("file://") ? opts._entry : pathToFileURL(resolve(opts._entry)).href;
111
+ const entryModule = await import(entryURL);
112
+ return {
113
+ fetch: () => renderError(`The entry file "${relative(".", opts._entry)}" does not export a valid fetch handler.`, 500, "Invalid Entry"),
114
+ ...entryModule.default,
115
+ ...opts
116
+ };
117
+ } catch (error) {
118
+ console.error(Colors.red(`${Colors.bold(opts._entry)}`));
119
+ if (error instanceof Error) Error.captureStackTrace?.(error, serve);
120
+ throw error;
121
+ }
122
+ }
123
+ function renderError(error, status = 500, title = "Server Error") {
124
+ let html = `<!DOCTYPE html><html><head><title>${title}</title></head><body>`;
125
+ if (options._prod) html += `<h1>${title}</h1><p>Something went wrong while processing your request.</p>`;
126
+ else html += `
127
+ <style>
128
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; color: #333; }
129
+ h1 { color: #dc3545; }
130
+ pre { background: #fff; padding: 10px; border-radius: 5px; overflow: auto; }
131
+ code { font-family: monospace; }
132
+ #error { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; }
133
+ </style>
134
+ <div id="error"><h1>${title}</h1><pre>${error instanceof Error ? error.stack || error.message : String(error)}</pre></div>
135
+ `;
136
+ return new Response(html, {
137
+ status,
138
+ headers: { "Content-Type": "text/html; charset=utf-8" }
139
+ });
140
+ }
141
+ function printInfo() {
142
+ if (options._entry) console.log(Colors.gray(`${Colors.bold(Colors.gray("λ"))} Server entry: ${Colors.cyan("./" + relative(".", options._entry))} ${options._prod ? "" : Colors.gray("(watching for changes)")}`));
143
+ if (options._static) console.log(Colors.gray(`${Colors.bold(Colors.gray("⊟"))} Static files: ${Colors.cyan("./" + relative(".", options._static) + "/")}`));
144
+ }
145
+ async function version() {
146
+ const { default: pkg } = await import("../package.json", { with: { type: "json" } });
147
+ return `srvx v${pkg.version}\n${runtime()}`;
148
+ }
149
+ function runtime() {
150
+ if (process.versions.bun) return `bun v${process.versions.bun}`;
151
+ else if (process.versions.deno) return `deno v${process.versions.deno}`;
152
+ else return `node v${process.versions.node}`;
153
+ }
154
+ function parseArgs$1(args$1) {
155
+ const { values, positionals } = parseArgs({
156
+ args: args$1,
157
+ allowPositionals: true,
158
+ options: {
159
+ help: {
160
+ type: "boolean",
161
+ short: "h"
162
+ },
163
+ version: {
164
+ type: "boolean",
165
+ short: "v"
166
+ },
167
+ prod: { type: "boolean" },
168
+ port: {
169
+ type: "string",
170
+ short: "p"
171
+ },
172
+ host: {
173
+ type: "string",
174
+ short: "H"
175
+ },
176
+ static: {
177
+ type: "string",
178
+ short: "s"
179
+ },
180
+ tls: { type: "boolean" },
181
+ cert: { type: "string" },
182
+ key: { type: "string" }
183
+ }
184
+ });
185
+ const input = positionals[0] || ".";
186
+ let dir;
187
+ let entry = "";
188
+ if (extname(input) === "") dir = resolve(input);
189
+ else {
190
+ entry = resolve(input);
191
+ dir = dirname(entry);
192
+ }
193
+ return {
194
+ _dir: dir,
195
+ _entry: entry,
196
+ _prod: values.prod ?? process.env.NODE_ENV === "production",
197
+ _help: values.help,
198
+ _static: values.static || "public",
199
+ _version: values.version,
200
+ port: values.port ? Number.parseInt(values.port, 10) : void 0,
201
+ hostname: values.host,
202
+ tls: values.tls ? {
203
+ cert: values.cert,
204
+ key: values.key
205
+ } : void 0
206
+ };
207
+ }
208
+ function example() {
209
+ const useTs = !options._entry || options._entry.endsWith(".ts");
210
+ return `${Colors.bold(Colors.gray("// server.ts"))}
211
+ ${Colors.magenta("export default")} {
212
+ ${Colors.cyan("fetch")}(req${useTs ? ": Request" : ""}) {
213
+ ${Colors.magenta("return")} new Response(${Colors.green("\"Hello, World!\"")});
214
+ }
215
+ }`;
216
+ }
217
+ function usage(mainOpts) {
218
+ const command = mainOpts.command;
219
+ return `
220
+ ${Colors.cyan(command)} - Start an HTTP server with the specified entry path.
221
+
222
+ ${Colors.bold("USAGE")}
223
+ ${existsSync(options._entry) ? "" : `\n${example()}\n`}
224
+ ${Colors.gray("# srvx [options] [entry]")}
225
+ ${Colors.gray("$")} ${Colors.cyan(command)} ${Colors.gray("./server.ts")} ${Colors.gray("# Start development server")}
226
+ ${Colors.gray("$")} ${Colors.cyan(command)} --prod ${Colors.gray("# Start production server")}
227
+ ${Colors.gray("$")} ${Colors.cyan(command)} --port=8080 ${Colors.gray("# Listen on port 8080")}
228
+ ${Colors.gray("$")} ${Colors.cyan(command)} --host=localhost ${Colors.gray("# Bind to localhost only")}
229
+ ${Colors.gray("$")} ${Colors.cyan(command)} --tls --cert=cert.pem --key=key.pem ${Colors.gray("# Enable TLS (HTTPS/HTTP2)")}
230
+
231
+ ${Colors.bold("ARGUMENTS")}
232
+
233
+ ${Colors.yellow("<entry>")} Server entry path to serve.
234
+ Default: ${defaultEntries.map((e) => Colors.cyan(e)).join(", ")} ${Colors.gray(`(${defaultExts.join(",")})`)}
235
+
236
+ ${Colors.bold("OPTIONS")}
237
+
238
+ ${Colors.green("-p, --port")} ${Colors.yellow("<port>")} Port to listen on (default: ${Colors.yellow("3000")})
239
+ ${Colors.green("--host")} ${Colors.yellow("<host>")} Host to bind to (default: all interfaces)
240
+ ${Colors.green("-s, --static")} ${Colors.yellow("<dir>")} Serve static files from the specified directory (default: ${Colors.yellow("public")})
241
+ ${Colors.green("--prod")} Run in production mode (no watch, no debug)
242
+ ${Colors.green("--tls")} Enable TLS (HTTPS/HTTP2)
243
+ ${Colors.green("--cert")} ${Colors.yellow("<file>")} TLS certificate file
244
+ ${Colors.green("--key")} ${Colors.yellow("<file>")} TLS private key file
245
+ ${Colors.green("-h, --help")} Show this help message
246
+ ${Colors.green("-v, --version")} Show server and runtime versions
247
+
248
+ ${Colors.bold("ENVIRONMENT")}
249
+
250
+ ${Colors.green("PORT")} Override port
251
+ ${Colors.green("HOST")} Override host
252
+ ${Colors.green("NODE_ENV")} Set to ${Colors.yellow("production")} for production mode.
253
+
254
+ ➤ ${Colors.url("Documentation", mainOpts.docs || "https://srvx.h3.dev")}
255
+ ➤ ${Colors.url("Report issues", mainOpts.issues || "https://github.com/h3js/srvx/issues")}
256
+ `.trim();
257
+ }
258
+ function setupProcessErrorHandlers() {
259
+ process.on("uncaughtException", (error) => {
260
+ console.error("Uncaught exception:", error);
261
+ process.exit(1);
262
+ });
263
+ process.on("unhandledRejection", (reason) => {
264
+ console.error("Unhandled rejection:", reason);
265
+ process.exit(1);
266
+ });
267
+ }
268
+
269
+ //#endregion
270
+ export { main, usage };
package/dist/log.d.mts ADDED
@@ -0,0 +1,8 @@
1
+ import { ServerMiddleware } from "./_chunks/types-GwbH0R0m.mjs";
2
+
3
+ //#region src/log.d.ts
4
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
5
+ interface LogOptions {}
6
+ declare const log: (_options?: LogOptions) => ServerMiddleware;
7
+ //#endregion
8
+ export { LogOptions, log };
package/dist/log.mjs ADDED
@@ -0,0 +1,15 @@
1
+ import { Colors } from "./_chunks/_utils.cli-4eOGyiAa.mjs";
2
+
3
+ //#region src/log.ts
4
+ const log = (_options = {}) => {
5
+ return async (req, next) => {
6
+ const start = performance.now();
7
+ const res = await next();
8
+ const duration = performance.now() - start;
9
+ console.log(`${Colors.gray(`[${new Date().toLocaleTimeString()}]`)} ${Colors.bold(req.method)} ${Colors.blue(req.url)} [${Colors[res.ok ? "green" : "red"](res.status + "")}] ${Colors.gray(`(${duration.toFixed(2)}ms)`)}`);
10
+ return res;
11
+ };
12
+ };
13
+
14
+ //#endregion
15
+ export { log };
@@ -0,0 +1,9 @@
1
+ import { ServerMiddleware } from "./_chunks/types-GwbH0R0m.mjs";
2
+
3
+ //#region src/static.d.ts
4
+ interface ServeStaticOptions {
5
+ dir: string;
6
+ }
7
+ declare const serveStatic: (options: ServeStaticOptions) => ServerMiddleware;
8
+ //#endregion
9
+ export { ServeStaticOptions, serveStatic };
@@ -0,0 +1,70 @@
1
+ import { extname, join, resolve } from "node:path";
2
+ import { createReadStream } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { FastResponse } from "srvx";
5
+ import { createBrotliCompress, createGzip } from "node:zlib";
6
+
7
+ //#region src/static.ts
8
+ const COMMON_MIME_TYPES = {
9
+ ".html": "text/html",
10
+ ".htm": "text/html",
11
+ ".css": "text/css",
12
+ ".js": "text/javascript",
13
+ ".mjs": "text/javascript",
14
+ ".json": "application/json",
15
+ ".txt": "text/plain",
16
+ ".xml": "application/xml",
17
+ ".gif": "image/gif",
18
+ ".ico": "image/vnd.microsoft.icon",
19
+ ".jpeg": "image/jpeg",
20
+ ".jpg": "image/jpeg",
21
+ ".png": "image/png",
22
+ ".svg": "image/svg+xml",
23
+ ".webp": "image/webp",
24
+ ".woff": "font/woff",
25
+ ".woff2": "font/woff2",
26
+ ".mp4": "video/mp4",
27
+ ".webm": "video/webm",
28
+ ".zip": "application/zip",
29
+ ".pdf": "application/pdf"
30
+ };
31
+ const serveStatic = (options) => {
32
+ const dir = resolve(options.dir) + "/";
33
+ return async (req, next) => {
34
+ if (req.method !== "GET" && req.method !== "HEAD") return next();
35
+ const path = new URL(req.url).pathname.slice(1).replace(/\/$/, "");
36
+ let paths;
37
+ if (path === "") paths = ["index.html"];
38
+ else if (extname(path) === "") paths = [`${path}.html`, `${path}/index.html`];
39
+ else paths = [path];
40
+ for (const path$1 of paths) {
41
+ const filePath = join(dir, path$1);
42
+ if (!filePath.startsWith(dir)) continue;
43
+ const fileStat = await stat(filePath).catch(() => null);
44
+ if (fileStat?.isFile()) {
45
+ const headers = {
46
+ "Content-Length": fileStat.size.toString(),
47
+ "Content-Type": COMMON_MIME_TYPES[extname(filePath)] || "application/octet-stream"
48
+ };
49
+ let stream = createReadStream(filePath);
50
+ const acceptEncoding = req.headers.get("accept-encoding") || "";
51
+ if (acceptEncoding.includes("br")) {
52
+ headers["Content-Encoding"] = "br";
53
+ delete headers["Content-Length"];
54
+ headers["Vary"] = "Accept-Encoding";
55
+ stream = stream.pipe(createBrotliCompress());
56
+ } else if (acceptEncoding.includes("gzip")) {
57
+ headers["Content-Encoding"] = "gzip";
58
+ delete headers["Content-Length"];
59
+ headers["Vary"] = "Accept-Encoding";
60
+ stream = stream.pipe(createGzip());
61
+ }
62
+ return new FastResponse(stream, { headers });
63
+ }
64
+ }
65
+ return next();
66
+ };
67
+ };
68
+
69
+ //#endregion
70
+ export { serveStatic };
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { BunFetchHandler, CloudflareFetchHandler, DenoFetchHandler, ErrorHandler, FastResponse, FastURL, FetchHandler, NodeHttpHandler, NodeServerRequest, NodeServerResponse, Server, ServerHandler, ServerMiddleware, ServerOptions, ServerPlugin, ServerRequest, ServerRuntimeContext, serve } from "./_chunks/types-IGvUeyh_.mjs";
1
+ import { BunFetchHandler, CloudflareFetchHandler, DenoFetchHandler, ErrorHandler, FastResponse, FastURL, FetchHandler, NodeHttpHandler, NodeServerRequest, NodeServerResponse, Server, ServerHandler, ServerMiddleware, ServerOptions, ServerPlugin, ServerRequest, ServerRuntimeContext, serve } from "./_chunks/types-GwbH0R0m.mjs";
2
2
  export { BunFetchHandler, CloudflareFetchHandler, DenoFetchHandler, ErrorHandler, FastResponse, FastURL, FetchHandler, NodeHttpHandler, NodeServerRequest, NodeServerResponse, Server, ServerHandler, ServerMiddleware, ServerOptions, ServerPlugin, ServerRequest, ServerRuntimeContext, serve };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "srvx",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Universal Server API based on web platform standards. Works seamlessly with Deno, Bun and Node.js.",
5
5
  "homepage": "https://srvx.h3.dev",
6
6
  "repository": "h3js/srvx",
@@ -14,6 +14,9 @@
14
14
  "./cloudflare": "./dist/adapters/cloudflare.mjs",
15
15
  "./generic": "./dist/adapters/generic.mjs",
16
16
  "./service-worker": "./dist/adapters/service-worker.mjs",
17
+ "./cli": "./dist/cli.mjs",
18
+ "./static": "./dist/static.mjs",
19
+ "./log": "./dist/log.mjs",
17
20
  ".": {
18
21
  "types": "./dist/types.d.mts",
19
22
  "deno": "./dist/adapters/deno.mjs",
@@ -25,7 +28,11 @@
25
28
  }
26
29
  },
27
30
  "types": "./dist/types.d.mts",
31
+ "bin": {
32
+ "srvx": "./bin/srvx.mjs"
33
+ },
28
34
  "files": [
35
+ "bin",
29
36
  "dist"
30
37
  ],
31
38
  "scripts": {
@@ -39,13 +46,9 @@
39
46
  "lint:fix": "automd && eslint . --fix && prettier -w .",
40
47
  "node-ts": "node --disable-warning=ExperimentalWarning --experimental-strip-types",
41
48
  "prepack": "pnpm build",
42
- "play:bun": "bun --watch playground/app.mjs",
43
- "play:cf": "pnpx wrangler dev playground/app.mjs",
44
- "play:deno": "deno run -A --watch playground/app.mjs",
45
49
  "play:mkcert": "openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj /CN=srvx.local",
46
- "play:node": "pnpm node-ts --watch playground/app.mjs",
47
- "play:sw": "pnpm build && pnpx serve playground",
48
50
  "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags",
51
+ "srvx": "./bin/srvx.mjs",
49
52
  "test": "pnpm lint && pnpm test:types && vitest run --coverage",
50
53
  "test:types": "tsc --noEmit --skipLibCheck"
51
54
  },
@@ -73,6 +76,7 @@
73
76
  "eslint-config-unjs": "^0.5.0",
74
77
  "execa": "^9.6.0",
75
78
  "get-port-please": "^3.1.2",
79
+ "mdbox": "^0.1.1",
76
80
  "mitata": "^1.0.34",
77
81
  "node-forge": "^1.3.1",
78
82
  "obuild": "^0.2.1",