srvx 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,170 +1,63 @@
1
- import { a as green, c as url, i as gray, l as yellow, n as bold, o as magenta, r as cyan, s as red } from "./_chunks/_color.mjs";
1
+ import { a as green, c as yellow, i as gray, n as bold, o as red, r as cyan, s as url } from "./_chunks/_utils.mjs";
2
+ import { r as loadServerEntry } from "./_chunks/loader.mjs";
2
3
  import { parseArgs } from "node:util";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
- import * as nodeHTTP$1 from "node:http";
5
- import { dirname, extname, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
6
5
  import { fork } from "node:child_process";
7
- import { existsSync } from "node:fs";
8
-
9
- //#region src/cli.ts
10
- const defaultEntries = [
11
- "server",
12
- "index",
13
- "src/server",
14
- "src/index",
15
- "server/index"
16
- ];
17
- const defaultExts = [
18
- ".mts",
19
- ".ts",
20
- ".cts",
21
- ".js",
22
- ".mjs",
23
- ".cjs",
24
- ".jsx",
25
- ".tsx"
26
- ];
27
- const args = process.argv.slice(2);
28
- const options = parseArgs$1(args);
29
- if (process.send) {
30
- setupProcessErrorHandlers();
31
- await serve();
32
- }
33
- async function main(mainOpts) {
34
- setupProcessErrorHandlers();
35
- if (options._version) {
36
- console.log(await version());
37
- process.exit(0);
38
- }
39
- if (options._help) {
40
- console.log(usage(mainOpts));
41
- process.exit(options._help ? 0 : 1);
42
- }
43
- const isBun = !!process.versions.bun;
44
- const isDeno = !!process.versions.deno;
45
- const isNode = !isBun && !isDeno;
46
- const runtimeArgs = [];
47
- if (!options._prod) runtimeArgs.push("--watch");
48
- if (isNode || isDeno) runtimeArgs.push(...[".env", options._prod ? ".env.production" : ".env.local"].filter((f) => existsSync(f)).map((f) => `--env-file=${f}`));
49
- if (isNode) {
50
- const [major, minor] = process.versions.node.split(".");
51
- if (major === "22" && +minor >= 6) runtimeArgs.push("--experimental-strip-types");
52
- if (options._import) runtimeArgs.push(`--import=${options._import}`);
53
- }
54
- const child = fork(fileURLToPath(import.meta.url), args, { execArgv: [...process.execArgv, ...runtimeArgs].filter(Boolean) });
55
- child.on("error", (error) => {
56
- console.error("Error in child process:", error);
57
- process.exit(1);
58
- });
59
- child.on("exit", (code) => {
60
- if (code !== 0) {
61
- console.error(`Child process exited with code ${code}`);
62
- process.exit(code);
63
- }
64
- });
65
- let cleanupCalled = false;
66
- const cleanup = (signal, exitCode) => {
67
- if (cleanupCalled) return;
68
- cleanupCalled = true;
69
- try {
70
- child.kill(signal || "SIGTERM");
71
- } catch (error) {
72
- console.error("Error killing child process:", error);
73
- }
74
- if (exitCode !== void 0) process.exit(exitCode);
75
- };
76
- process.on("exit", () => cleanup("SIGTERM"));
77
- process.on("SIGINT", () => cleanup("SIGINT", 130));
78
- process.on("SIGTERM", () => cleanup("SIGTERM", 143));
79
- }
80
- async function serve() {
6
+ import { createReadStream, existsSync, statSync } from "node:fs";
7
+ import { dirname, relative, resolve } from "node:path";
8
+ import { Readable } from "node:stream";
9
+ var version = "0.11.0";
10
+ const NO_ENTRY_ERROR = "No server entry or public directory found";
11
+ async function cliServe(cliOpts) {
81
12
  try {
82
- if (!process.env.NODE_ENV) process.env.NODE_ENV = options._prod ? "production" : "development";
83
- const entry = await loadEntry(options);
84
- const { serve: srvxServe } = entry._legacyNode ? await import("srvx/node") : await import("srvx");
13
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = cliOpts.prod ? "production" : "development";
14
+ const loaded = await loadServerEntry({
15
+ entry: cliOpts.entry,
16
+ dir: cliOpts.dir
17
+ });
18
+ const { serve: srvxServe } = loaded.nodeCompat ? await import("srvx/node") : await import("srvx");
85
19
  const { serveStatic } = await import("srvx/static");
86
20
  const { log } = await import("srvx/log");
87
- const staticDir = resolve(options._dir, options._static);
88
- options._static = existsSync(staticDir) ? staticDir : "";
89
- const server = srvxServe({
21
+ const staticDir = resolve(cliOpts.dir || (loaded.url ? dirname(fileURLToPath(loaded.url)) : "."), cliOpts.static || "public");
22
+ cliOpts.static = existsSync(staticDir) ? staticDir : "";
23
+ if (loaded.notFound && !cliOpts.static) {
24
+ process.send?.({ error: "no-entry" });
25
+ throw new Error(NO_ENTRY_ERROR, { cause: cliOpts });
26
+ }
27
+ const serverOptions = {
28
+ ...loaded.module?.default,
29
+ default: void 0,
30
+ ...loaded.module
31
+ };
32
+ printInfo(cliOpts, loaded);
33
+ await (globalThis.__srvx__ = srvxServe({
34
+ ...serverOptions,
35
+ gracefulShutdown: cliOpts.prod,
36
+ port: cliOpts.port ?? serverOptions.port,
37
+ hostname: cliOpts.hostname ?? cliOpts.host ?? serverOptions.hostname,
38
+ tls: cliOpts.tls ? {
39
+ cert: cliOpts.cert,
40
+ key: cliOpts.key
41
+ } : void 0,
90
42
  error: (error) => {
91
43
  console.error(error);
92
- return renderError(error);
44
+ return renderError(cliOpts, error);
93
45
  },
94
- ...entry,
46
+ fetch: loaded.fetch || (() => renderError(cliOpts, loaded.notFound ? "Server Entry Not Found" : "No Fetch Handler Exported", 501)),
95
47
  middleware: [
96
48
  log(),
97
- options._static ? serveStatic({ dir: options._static }) : void 0,
98
- ...entry.middleware || []
49
+ cliOpts.static ? serveStatic({ dir: cliOpts.static }) : void 0,
50
+ ...serverOptions.middleware || []
99
51
  ].filter(Boolean)
100
- });
101
- globalThis.__srvx__ = server;
102
- await server.ready();
103
- await globalThis.__srvx_listen_cb__?.();
104
- printInfo(entry);
52
+ })).ready();
105
53
  } catch (error) {
106
54
  console.error(error);
107
55
  process.exit(1);
108
56
  }
109
57
  }
110
- async function loadEntry(opts) {
111
- try {
112
- if (!opts._entry) for (const entry of defaultEntries) {
113
- for (const ext of defaultExts) {
114
- const entryPath = resolve(opts._dir, `${entry}${ext}`);
115
- if (existsSync(entryPath)) {
116
- opts._entry = entryPath;
117
- break;
118
- }
119
- }
120
- if (opts._entry) break;
121
- }
122
- if (!opts._entry) {
123
- const _error$1 = `No server entry file found.\nPlease specify an entry file or ensure one of the default entries exists (${defaultEntries.join(", ")}).`;
124
- return {
125
- _error: _error$1,
126
- fetch: () => renderError(_error$1, 404, "No Server Entry"),
127
- ...opts
128
- };
129
- }
130
- const entryURL = opts._entry.startsWith("file://") ? opts._entry : pathToFileURL(resolve(opts._entry)).href;
131
- const { res: mod, listenHandler } = await interceptListen(() => import(entryURL));
132
- let fetchHandler = mod.fetch || mod.default?.fetch || mod.default?.default?.fetch;
133
- let _legacyNode = false;
134
- if (!fetchHandler) {
135
- const nodeHandler = listenHandler || (typeof mod.default === "function" ? mod.default : void 0);
136
- if (nodeHandler) {
137
- _legacyNode = true;
138
- const { callNodeHandler } = await import("./_chunks/call2.mjs");
139
- fetchHandler = (webReq) => callNodeHandler(nodeHandler, webReq);
140
- }
141
- }
142
- let _error;
143
- if (!fetchHandler) {
144
- _error = `The entry file "${relative(".", opts._entry)}" does not export a valid fetch handler.`;
145
- fetchHandler = () => renderError(_error, 500, "Invalid Entry");
146
- }
147
- return {
148
- ...mod,
149
- ...mod.default,
150
- ...opts,
151
- _error,
152
- _legacyNode,
153
- fetch: fetchHandler
154
- };
155
- } catch (error) {
156
- if (error?.code === "ERR_UNKNOWN_FILE_EXTENSION") {
157
- const message = String(error);
158
- if (/"\.(m|c)?ts"/g.test(message)) console.error(red(`\nMake sure you're using Node.js v22.18+ or v24+ for TypeScript support (current version: ${process.versions.node})\n\n`));
159
- else if (/"\.(m|c)?tsx"/g.test(message)) console.error(red(`\nYou need a compatible loader for JSX support (Deno, Bun or srvx --register jiti/register)\n\n`));
160
- }
161
- if (error instanceof Error) Error.captureStackTrace?.(error, serve);
162
- throw error;
163
- }
164
- }
165
- function renderError(error, status = 500, title = "Server Error") {
58
+ function renderError(cliOpts, error, status = 500, title = "Server Error") {
166
59
  let html = `<!DOCTYPE html><html><head><title>${title}</title></head><body>`;
167
- if (options._prod) html += `<h1>${title}</h1><p>Something went wrong while processing your request.</p>`;
60
+ if (cliOpts.prod) html += `<h1>${title}</h1><p>Something went wrong while processing your request.</p>`;
168
61
  else html += `
169
62
  <style>
170
63
  body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; color: #333; }
@@ -180,139 +73,141 @@ function renderError(error, status = 500, title = "Server Error") {
180
73
  headers: { "Content-Type": "text/html; charset=utf-8" }
181
74
  });
182
75
  }
183
- function printInfo(entry) {
76
+ function printInfo(cliOpts, loaded) {
184
77
  let entryInfo;
185
- if (options._entry) entryInfo = cyan("./" + relative(".", options._entry));
186
- else entryInfo = gray(`(create ${bold(`server.ts`)} to enable)`);
187
- console.log(gray(`${bold(gray("λ"))} Server handler: ${entryInfo}`));
188
- if (options._entry && entry._error) console.error(red(` ${entry._error}`));
78
+ if (loaded.notFound) entryInfo = gray(`(create ${bold(`server.ts`)})`);
79
+ else entryInfo = loaded.fetch ? cyan("./" + relative(".", fileURLToPath(loaded.url))) : red(`No fetch handler exported from ${loaded.url}`);
80
+ console.log(gray(`${bold(gray(""))} Server handler: ${entryInfo}`));
189
81
  let staticInfo;
190
- if (options._static) staticInfo = cyan("./" + relative(".", options._static) + "/");
191
- else staticInfo = gray(`(add ${bold("public/")} dir to enable)`);
192
- console.log(gray(`${bold(gray(""))} Static files: ${staticInfo}`));
82
+ if (cliOpts.static) staticInfo = cyan("./" + relative(".", cliOpts.static) + "/");
83
+ else staticInfo = gray(`(create ${bold("public/")} dir)`);
84
+ console.log(gray(`${bold(gray(""))} Static files: ${staticInfo}`));
85
+ console.log("");
193
86
  }
194
- async function interceptListen(cb) {
195
- const originalListen = nodeHTTP$1.Server.prototype.listen;
196
- let res;
197
- let listenHandler;
198
- try {
199
- nodeHTTP$1.Server.prototype.listen = function(arg1, arg2) {
200
- listenHandler = this._events.request;
201
- if (Array.isArray(listenHandler)) listenHandler = listenHandler[0];
202
- nodeHTTP$1.Server.prototype.listen = originalListen;
203
- globalThis.__srvx_listen_cb__ = [arg1, arg2].find((arg) => typeof arg === "function");
204
- return new Proxy({}, { get(_, prop) {
205
- return globalThis.__srvx__?.node?.server?.[prop];
206
- } });
207
- };
208
- res = await cb();
209
- } finally {
210
- nodeHTTP$1.Server.prototype.listen = originalListen;
87
+ async function cliFetch(cliOpts) {
88
+ const stdin = cliOpts.stdin || process.stdin;
89
+ const stdout = cliOpts.stdout || process.stdout;
90
+ const stderr = cliOpts.stderr || process.stderr;
91
+ let fetchHandler = globalThis.fetch;
92
+ let inputURL = cliOpts.url || "/";
93
+ if (inputURL[0] === "/") {
94
+ const loaded = await loadServerEntry({
95
+ dir: cliOpts.dir,
96
+ entry: cliOpts.entry,
97
+ ...cliOpts?.loader
98
+ });
99
+ if (cliOpts.verbose && loaded.url) {
100
+ stderr.write(`* Entry: ${fileURLToPath(loaded.url)}\n`);
101
+ if (loaded.nodeCompat) stderr.write(`* Using node compat mode\n`);
102
+ }
103
+ if (loaded.notFound) throw new Error(`Server entry file not found in ${resolve(cliOpts.dir || ".")}`, { cause: {
104
+ dir: cliOpts.dir || process.cwd(),
105
+ entry: cliOpts.entry || void 0
106
+ } });
107
+ else if (!loaded.fetch) throw new Error("No fetch handler exported", { cause: {
108
+ dir: cliOpts.dir || process.cwd(),
109
+ entry: cliOpts.entry || void 0,
110
+ loaded
111
+ } });
112
+ fetchHandler = loaded.fetch;
113
+ } else {
114
+ stderr.write(`* Fetching remote URL: ${inputURL}\n`);
115
+ if (!URL?.canParse(inputURL)) inputURL = `http${cliOpts.tls ? "s" : ""}://${inputURL}`;
116
+ fetchHandler = globalThis.fetch;
211
117
  }
212
- return {
213
- res,
214
- listenHandler
215
- };
216
- }
217
- async function version() {
218
- return `srvx ${globalThis.__srvx_version__ || "unknown"}\n${runtime()}`;
219
- }
220
- function runtime() {
221
- if (process.versions.bun) return `bun ${process.versions.bun}`;
222
- else if (process.versions.deno) return `deno ${process.versions.deno}`;
223
- else return `node ${process.versions.node}`;
224
- }
225
- function parseArgs$1(args$1) {
226
- const { values, positionals } = parseArgs({
227
- args: args$1,
228
- allowPositionals: true,
229
- options: {
230
- help: {
231
- type: "boolean",
232
- short: "h"
233
- },
234
- version: {
235
- type: "boolean",
236
- short: "v"
237
- },
238
- prod: { type: "boolean" },
239
- port: {
240
- type: "string",
241
- short: "p"
242
- },
243
- host: {
244
- type: "string",
245
- short: "H"
246
- },
247
- static: {
248
- type: "string",
249
- short: "s"
250
- },
251
- import: { type: "string" },
252
- tls: { type: "boolean" },
253
- cert: { type: "string" },
254
- key: { type: "string" }
118
+ const headers = new Headers();
119
+ if (cliOpts.header) for (const header of cliOpts.header) {
120
+ const colonIndex = header.indexOf(":");
121
+ if (colonIndex > 0) {
122
+ const name = header.slice(0, colonIndex).trim();
123
+ const value = header.slice(colonIndex + 1).trim();
124
+ headers.append(name, value);
255
125
  }
126
+ }
127
+ if (!headers.has("User-Agent")) headers.set("User-Agent", "srvx (curl)");
128
+ if (!headers.has("Accept")) headers.set("Accept", "text/markdown, application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7, text/*;q=0.6, */*;q=0.5");
129
+ let body;
130
+ if (cliOpts.data !== void 0) if (cliOpts.data === "@-") body = new ReadableStream({ async start(controller) {
131
+ for await (const chunk of stdin) controller.enqueue(chunk);
132
+ controller.close();
133
+ } });
134
+ else if (cliOpts.data.startsWith("@")) body = Readable.toWeb(createReadStream(cliOpts.data.slice(1)));
135
+ else body = cliOpts.data;
136
+ const method = cliOpts.method || (body === void 0 ? "GET" : "POST");
137
+ const url = new URL(inputURL, `http${cliOpts.tls ? "s" : ""}://${cliOpts.host || cliOpts.hostname || "localhost"}`);
138
+ const req = new Request(url, {
139
+ method,
140
+ headers,
141
+ body
256
142
  });
257
- const input = positionals[0] || ".";
258
- let dir;
259
- let entry = "";
260
- if (extname(input) === "") dir = resolve(input);
261
- else {
262
- entry = resolve(input);
263
- dir = dirname(entry);
143
+ if (cliOpts.verbose) {
144
+ const parsedUrl = new URL(url);
145
+ stderr.write(`> ${method} ${parsedUrl.pathname}${parsedUrl.search} HTTP/1.1\n`);
146
+ stderr.write(`> Host: ${parsedUrl.host}\n`);
147
+ for (const [name, value] of headers) stderr.write(`> ${name}: ${value}\n`);
148
+ stderr.write(">\n");
264
149
  }
265
- if (!existsSync(dir)) {
266
- console.error(red(`Directory "${dir}" does not exist.\n`));
267
- process.exit(1);
150
+ const res = await fetchHandler(req);
151
+ if (cliOpts.verbose) {
152
+ stderr.write(`< HTTP/1.1 ${res.status} ${res.statusText}\n`);
153
+ for (const [name, value] of res.headers) stderr.write(`< ${name}: ${value}\n`);
154
+ stderr.write("<\n");
268
155
  }
156
+ if (res.body) {
157
+ const { isBinary, encoding } = getResponseFormat(res);
158
+ if (isBinary) for await (const chunk of res.body) stdout.write(chunk);
159
+ else {
160
+ const decoder = new TextDecoder(encoding);
161
+ for await (const chunk of res.body) stdout.write(decoder.decode(chunk, { stream: true }));
162
+ const remaining = decoder.decode();
163
+ if (remaining) stdout.write(remaining);
164
+ if (stdout.isTTY) stdout.write("\n");
165
+ }
166
+ }
167
+ return res;
168
+ }
169
+ function getResponseFormat(res) {
170
+ const contentType = res.headers.get("content-type") || "";
269
171
  return {
270
- _dir: dir,
271
- _entry: entry,
272
- _prod: values.prod ?? process.env.NODE_ENV === "production",
273
- _help: values.help,
274
- _static: values.static || "public",
275
- _version: values.version,
276
- _import: values.import,
277
- port: values.port ? Number.parseInt(values.port, 10) : void 0,
278
- hostname: values.host,
279
- tls: values.tls ? {
280
- cert: values.cert,
281
- key: values.key
282
- } : void 0
172
+ isBinary: contentType.startsWith("application/octet-stream") || contentType.startsWith("image/") || contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.startsWith("application/pdf") || contentType.startsWith("application/zip") || contentType.startsWith("application/gzip"),
173
+ encoding: contentType.includes("charset=") ? contentType.split("charset=")[1].split(";")[0].trim() : "utf8"
283
174
  };
284
175
  }
285
- function example() {
286
- const useTs = !options._entry || options._entry.endsWith(".ts");
287
- return `${bold(gray("// server.ts"))}
288
- ${magenta("export default")} {
289
- ${cyan("fetch")}(req${useTs ? ": Request" : ""}) {
290
- ${magenta("return")} new Response(${green("\"Hello, World!\"")});
291
- }
292
- }`;
293
- }
294
176
  function usage(mainOpts) {
295
- const command = mainOpts.command;
177
+ const command = mainOpts.usage?.command || "srvx";
296
178
  return `
297
- ${cyan(command)} - Start an HTTP server with the specified entry path.
179
+ ${cyan(command)} - Universal Server CLI
180
+
181
+ ${bold("SERVE MODE")}
298
182
 
299
- ${bold("USAGE")}
300
- ${existsSync(options._entry) ? "" : `\n${example()}\n`}
301
- ${gray("# srvx [options] [entry]")}
302
- ${gray("$")} ${cyan(command)} ${gray("./server.ts")} ${gray("# Start development server")}
303
- ${gray("$")} ${cyan(command)} --prod ${gray("# Start production server")}
304
- ${gray("$")} ${cyan(command)} --port=8080 ${gray("# Listen on port 8080")}
305
- ${gray("$")} ${cyan(command)} --host=localhost ${gray("# Bind to localhost only")}
306
- ${gray("$")} ${cyan(command)} --import=jiti/register ${gray(`# Enable ${url("jiti", "https://github.com/unjs/jiti")} loader`)}
307
- ${gray("$")} ${cyan(command)} --tls --cert=cert.pem --key=key.pem ${gray("# Enable TLS (HTTPS/HTTP2)")}
183
+ ${bold(green(`# ${command} serve [options]`))}
184
+ ${gray("$")} ${cyan(command)} serve --entry ${gray("./server.ts")} ${gray("# Start development server")}
185
+ ${gray("$")} ${cyan(command)} serve --prod ${gray("# Start production server")}
186
+ ${gray("$")} ${cyan(command)} serve --port=8080 ${gray("# Listen on port 8080")}
187
+ ${gray("$")} ${cyan(command)} serve --host=localhost ${gray("# Bind to localhost only")}
188
+ ${gray("$")} ${cyan(command)} serve --import=jiti/register ${gray(`# Enable ${url("jiti", "https://github.com/unjs/jiti")} loader`)}
189
+ ${gray("$")} ${cyan(command)} serve --tls --cert=cert.pem --key=key.pem ${gray("# Enable TLS (HTTPS/HTTP2)")}
308
190
 
191
+ ${bold("FETCH MODE")}
309
192
 
310
- ${bold("ARGUMENTS")}
193
+ ${bold(green(`# ${command} fetch|curl [options] [url]`))}
194
+ ${gray("$")} ${cyan(command)} fetch ${gray("# Fetch from default entry")}
195
+ ${gray("$")} ${cyan(command)} fetch /api/users ${gray("# Fetch a specific URL/path")}
196
+ ${gray("$")} ${cyan(command)} fetch --entry ./server.ts /api/users ${gray("# Fetch using a specific entry")}
197
+ ${gray("$")} ${cyan(command)} fetch -X POST /api/users ${gray("# POST request")}
198
+ ${gray("$")} ${cyan(command)} fetch -H "Content-Type: application/json" /api ${gray("# With headers")}
199
+ ${gray("$")} ${cyan(command)} fetch -d '{"name":"foo"}' /api ${gray("# With request body")}
200
+ ${gray("$")} ${cyan(command)} fetch -v /api/users ${gray("# Verbose output (show headers)")}
201
+ ${gray("$")} echo '{"name":"foo"}' | ${cyan(command)} fetch -d @- /api ${gray("# Body from stdin")}
311
202
 
312
- ${yellow("<entry>")} Server entry path to serve.
313
- Default: ${defaultEntries.map((e) => cyan(e)).join(", ")} ${gray(`(${defaultExts.join(",")})`)}
203
+ ${bold("COMMON OPTIONS")}
314
204
 
315
- ${bold("OPTIONS")}
205
+ ${green("--entry")} ${yellow("<file>")} Server entry file to use
206
+ ${green("--dir")} ${yellow("<dir>")} Working directory for resolving entry file
207
+ ${green("-h, --help")} Show this help message
208
+ ${green("--version")} Show server and runtime versions
209
+
210
+ ${bold("SERVE OPTIONS")}
316
211
 
317
212
  ${green("-p, --port")} ${yellow("<port>")} Port to listen on (default: ${yellow("3000")})
318
213
  ${green("--host")} ${yellow("<host>")} Host to bind to (default: all interfaces)
@@ -322,8 +217,13 @@ ${bold("OPTIONS")}
322
217
  ${green("--tls")} Enable TLS (HTTPS/HTTP2)
323
218
  ${green("--cert")} ${yellow("<file>")} TLS certificate file
324
219
  ${green("--key")} ${yellow("<file>")} TLS private key file
325
- ${green("-h, --help")} Show this help message
326
- ${green("-v, --version")} Show server and runtime versions
220
+
221
+ ${bold("FETCH OPTIONS")}
222
+
223
+ ${green("-X, --request")} ${yellow("<method>")} HTTP method (default: ${yellow("GET")}, or ${yellow("POST")} if body is provided)
224
+ ${green("-H, --header")} ${yellow("<header>")} Add header (format: "Name: Value", can be used multiple times)
225
+ ${green("-d, --data")} ${yellow("<data>")} Request body (use ${yellow("@-")} for stdin, ${yellow("@file")} for file)
226
+ ${green("-v, --verbose")} Show request and response headers
327
227
 
328
228
  ${bold("ENVIRONMENT")}
329
229
 
@@ -331,10 +231,158 @@ ${bold("ENVIRONMENT")}
331
231
  ${green("HOST")} Override host
332
232
  ${green("NODE_ENV")} Set to ${yellow("production")} for production mode.
333
233
 
334
- ${url("Documentation", mainOpts.docs || "https://srvx.h3.dev")}
335
- ${url("Report issues", mainOpts.issues || "https://github.com/h3js/srvx/issues")}
234
+ ${mainOpts.usage?.docs ? `➤ ${url("Documentation", mainOpts.usage.docs)}` : ""}
235
+ ${mainOpts.usage?.issues ? `➤ ${url("Report issues", mainOpts.usage.issues)}` : ""}
336
236
  `.trim();
337
237
  }
238
+ async function main(mainOpts) {
239
+ const args = process.argv.slice(2);
240
+ const cliOpts = parseArgs$1(args);
241
+ if (cliOpts.version) {
242
+ console.log(`srvx ${version}\n${runtime()}`);
243
+ process.exit(0);
244
+ }
245
+ if (cliOpts.help) {
246
+ console.log(usage(mainOpts));
247
+ process.exit(cliOpts.help ? 0 : 1);
248
+ }
249
+ if (process.send) {
250
+ console.log(gray(`srvx ${version} - ${runtime()}`));
251
+ setupProcessErrorHandlers();
252
+ await cliServe(cliOpts);
253
+ return;
254
+ }
255
+ if (cliOpts.mode === "fetch") try {
256
+ const res = await cliFetch(cliOpts);
257
+ process.exit(res.ok ? 0 : 22);
258
+ } catch (error) {
259
+ console.error(error);
260
+ process.exit(1);
261
+ }
262
+ const isBun = !!process.versions.bun;
263
+ const isDeno = !!process.versions.deno;
264
+ const isNode = !isBun && !isDeno;
265
+ const runtimeArgs = [];
266
+ if (!cliOpts.prod) runtimeArgs.push("--watch");
267
+ if (isNode || isDeno) runtimeArgs.push(...[".env", cliOpts.prod ? ".env.production" : ".env.local"].filter((f) => existsSync(f)).map((f) => `--env-file=${f}`));
268
+ if (isNode) {
269
+ const [major, minor] = process.versions.node.split(".");
270
+ if (major === "22" && +minor >= 6) runtimeArgs.push("--experimental-strip-types");
271
+ if (cliOpts.import) runtimeArgs.push(`--import=${cliOpts.import}`);
272
+ }
273
+ const child = fork(fileURLToPath(new URL("../bin/srvx.mjs", import.meta.url)), args, { execArgv: [...process.execArgv, ...runtimeArgs].filter(Boolean) });
274
+ child.on("error", (error) => {
275
+ console.error("Error in child process:", error);
276
+ process.exit(1);
277
+ });
278
+ child.on("exit", (code) => {
279
+ if (code !== 0) {
280
+ console.error(`Child process exited with code ${code}`);
281
+ process.exit(code);
282
+ }
283
+ });
284
+ child.on("message", (msg) => {
285
+ if (msg && msg.error === "no-entry") {
286
+ console.error("\n" + red(NO_ENTRY_ERROR) + "\n");
287
+ process.exit(3);
288
+ }
289
+ });
290
+ let cleanupCalled = false;
291
+ const cleanup = (signal, exitCode) => {
292
+ if (cleanupCalled) return;
293
+ cleanupCalled = true;
294
+ try {
295
+ child.kill(signal || "SIGTERM");
296
+ } catch (error) {
297
+ console.error("Error killing child process:", error);
298
+ }
299
+ if (exitCode !== void 0) process.exit(exitCode);
300
+ };
301
+ process.on("exit", () => cleanup("SIGTERM"));
302
+ process.on("SIGINT", () => cleanup("SIGINT", 130));
303
+ process.on("SIGTERM", () => cleanup("SIGTERM", 143));
304
+ }
305
+ function parseArgs$1(args) {
306
+ const pArg0 = args.find((a) => !a.startsWith("-"));
307
+ const mode = pArg0 === "fetch" || pArg0 === "curl" ? "fetch" : "serve";
308
+ const commonArgs = {
309
+ help: { type: "boolean" },
310
+ version: { type: "boolean" },
311
+ dir: { type: "string" },
312
+ entry: { type: "string" },
313
+ host: { type: "string" },
314
+ hostname: { type: "string" },
315
+ tls: { type: "boolean" }
316
+ };
317
+ if (mode === "serve") {
318
+ const { values, positionals } = parseArgs({
319
+ args,
320
+ allowPositionals: true,
321
+ options: {
322
+ ...commonArgs,
323
+ url: { type: "string" },
324
+ prod: { type: "boolean" },
325
+ port: {
326
+ type: "string",
327
+ short: "p"
328
+ },
329
+ static: {
330
+ type: "string",
331
+ short: "s"
332
+ },
333
+ import: { type: "string" },
334
+ cert: { type: "string" },
335
+ key: { type: "string" }
336
+ }
337
+ });
338
+ if (positionals[0] === "serve") positionals.shift();
339
+ const maybeEntryOrDir = positionals[0];
340
+ if (maybeEntryOrDir) {
341
+ if (values.entry || values.dir) throw new Error("Cannot specify entry or dir as positional argument when --entry or --dir is used!");
342
+ if (statSync(maybeEntryOrDir).isDirectory()) values.dir = maybeEntryOrDir;
343
+ else values.entry = maybeEntryOrDir;
344
+ }
345
+ return {
346
+ mode,
347
+ ...values
348
+ };
349
+ }
350
+ const { values, positionals } = parseArgs({
351
+ args,
352
+ allowPositionals: true,
353
+ options: {
354
+ ...commonArgs,
355
+ url: { type: "string" },
356
+ method: {
357
+ type: "string",
358
+ short: "X"
359
+ },
360
+ request: { type: "string" },
361
+ header: {
362
+ type: "string",
363
+ multiple: true,
364
+ short: "H"
365
+ },
366
+ verbose: {
367
+ type: "boolean",
368
+ short: "v"
369
+ },
370
+ data: {
371
+ type: "string",
372
+ short: "d"
373
+ }
374
+ }
375
+ });
376
+ if (positionals[0] === "fetch" || positionals[0] === "curl") positionals.shift();
377
+ const method = values.method || values.request;
378
+ const url = values.url || positionals[0] || "/";
379
+ return {
380
+ mode,
381
+ ...values,
382
+ url,
383
+ method
384
+ };
385
+ }
338
386
  function setupProcessErrorHandlers() {
339
387
  process.on("uncaughtException", (error) => {
340
388
  console.error("Uncaught exception:", error);
@@ -345,6 +393,9 @@ function setupProcessErrorHandlers() {
345
393
  process.exit(1);
346
394
  });
347
395
  }
348
-
349
- //#endregion
350
- export { main, usage };
396
+ function runtime() {
397
+ if (process.versions.bun) return `bun ${process.versions.bun}`;
398
+ else if (process.versions.deno) return `deno ${process.versions.deno}`;
399
+ else return `node ${process.versions.node}`;
400
+ }
401
+ export { cliFetch, main };
@@ -0,0 +1,2 @@
1
+ import { a as loadServerEntry, i as defaultExts, n as LoadedServerEntry, r as defaultEntries, t as LoadOptions } from "./_chunks/loader.mjs";
2
+ export { LoadOptions, LoadedServerEntry, defaultEntries, defaultExts, loadServerEntry };
@@ -0,0 +1,2 @@
1
+ import { n as defaultExts, r as loadServerEntry, t as defaultEntries } from "./_chunks/loader.mjs";
2
+ export { defaultEntries, defaultExts, loadServerEntry };
package/dist/log.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { v as ServerMiddleware } from "./_chunks/types.mjs";
1
+ import { ServerMiddleware } from "./types.mjs";
2
2
 
3
3
  //#region src/log.d.ts
4
4
  interface LogOptions {}