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/README.md +6 -20
- package/bin/srvx.mjs +5 -3
- package/dist/_chunks/_plugins.mjs +2 -9
- package/dist/_chunks/_url.mjs +33 -25
- package/dist/_chunks/_utils.mjs +14 -73
- package/dist/_chunks/_utils2.mjs +70 -0
- package/dist/_chunks/loader.d.mts +74 -0
- package/dist/_chunks/loader.mjs +108 -0
- package/dist/adapters/aws-lambda.d.mts +19 -0
- package/dist/adapters/aws-lambda.mjs +292 -0
- package/dist/adapters/bun.d.mts +1 -1
- package/dist/adapters/bun.mjs +2 -6
- package/dist/adapters/cloudflare.d.mts +1 -1
- package/dist/adapters/cloudflare.mjs +1 -5
- package/dist/adapters/deno.d.mts +1 -1
- package/dist/adapters/deno.mjs +2 -6
- package/dist/adapters/generic.d.mts +1 -1
- package/dist/adapters/generic.mjs +2 -6
- package/dist/adapters/node.d.mts +1 -1
- package/dist/adapters/node.mjs +164 -84
- package/dist/adapters/service-worker.d.mts +1 -1
- package/dist/adapters/service-worker.mjs +1 -5
- package/dist/cli.d.mts +46 -11
- package/dist/cli.mjs +323 -272
- package/dist/loader.d.mts +2 -0
- package/dist/loader.mjs +2 -0
- package/dist/log.d.mts +1 -1
- package/dist/log.mjs +2 -6
- package/dist/static.d.mts +1 -1
- package/dist/static.mjs +4 -8
- package/dist/tracing.d.mts +1 -2
- package/dist/tracing.mjs +1 -25
- package/dist/types.d.mts +302 -1
- package/package.json +37 -34
- package/dist/_chunks/_color.mjs +0 -18
- package/dist/_chunks/_inherit.mjs +0 -31
- package/dist/_chunks/call.mjs +0 -157
- package/dist/_chunks/call2.mjs +0 -3
- package/dist/_chunks/types.d.mts +0 -283
package/dist/cli.mjs
CHANGED
|
@@ -1,170 +1,63 @@
|
|
|
1
|
-
import { a as green, c as
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 =
|
|
83
|
-
const
|
|
84
|
-
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
46
|
+
fetch: loaded.fetch || (() => renderError(cliOpts, loaded.notFound ? "Server Entry Not Found" : "No Fetch Handler Exported", 501)),
|
|
95
47
|
middleware: [
|
|
96
48
|
log(),
|
|
97
|
-
|
|
98
|
-
...
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
76
|
+
function printInfo(cliOpts, loaded) {
|
|
184
77
|
let entryInfo;
|
|
185
|
-
if (
|
|
186
|
-
else entryInfo =
|
|
187
|
-
console.log(gray(`${bold(gray("
|
|
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 (
|
|
191
|
-
else staticInfo = gray(`(
|
|
192
|
-
console.log(gray(`${bold(gray("
|
|
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
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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)} -
|
|
179
|
+
${cyan(command)} - Universal Server CLI
|
|
180
|
+
|
|
181
|
+
${bold("SERVE MODE")}
|
|
298
182
|
|
|
299
|
-
${bold(
|
|
300
|
-
${
|
|
301
|
-
${gray("
|
|
302
|
-
${gray("$")} ${cyan(command)}
|
|
303
|
-
${gray("$")} ${cyan(command)} --
|
|
304
|
-
${gray("$")} ${cyan(command)} --
|
|
305
|
-
${gray("$")} ${cyan(command)} --
|
|
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(
|
|
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
|
-
|
|
313
|
-
Default: ${defaultEntries.map((e) => cyan(e)).join(", ")} ${gray(`(${defaultExts.join(",")})`)}
|
|
203
|
+
${bold("COMMON OPTIONS")}
|
|
314
204
|
|
|
315
|
-
${
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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 };
|
package/dist/loader.mjs
ADDED
package/dist/log.d.mts
CHANGED