import { Colors } from "./_chunks/_utils.cli-B2YzwlOv.mjs";
import { parseArgs } from "node:util";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname, extname, relative, resolve } from "node:path";
import { fork } from "node:child_process";
import { existsSync } from "node:fs";
//#region src/cli.ts
const defaultEntries = [
"server",
"index",
"src/server",
"src/index",
"server/index"
];
const defaultExts = [
".mts",
".ts",
".cts",
".js",
".mjs",
".cjs",
".jsx",
".tsx"
];
const args = process.argv.slice(2);
const options = parseArgs$1(args);
if (process.send) {
setupProcessErrorHandlers();
await serve();
}
async function main(mainOpts) {
setupProcessErrorHandlers();
if (options._version) {
console.log(await version());
process.exit(0);
}
if (options._help) {
console.log(usage(mainOpts));
process.exit(options._help ? 0 : 1);
}
const isBun = !!process.versions.bun;
const isDeno = !!process.versions.deno;
const isNode = !isBun && !isDeno;
const runtimeArgs = [];
if (!options._prod) runtimeArgs.push("--watch");
if (isNode || isDeno) runtimeArgs.push(...[".env", options._prod ? ".env.production" : ".env.local"].filter((f) => existsSync(f)).map((f) => `--env-file=${f}`));
if (isNode) {
const [major, minor] = process.versions.node.split(".");
if (major === "22" && +minor >= 6) runtimeArgs.push("--experimental-strip-types");
if (options._import) runtimeArgs.push(`--import=${options._import}`);
}
const child = fork(fileURLToPath(import.meta.url), args, { execArgv: [...process.execArgv, ...runtimeArgs].filter(Boolean) });
child.on("error", (error) => {
console.error("Error in child process:", error);
process.exit(1);
});
child.on("exit", (code) => {
if (code !== 0) {
console.error(`Child process exited with code ${code}`);
process.exit(code);
}
});
}
async function serve() {
try {
if (!process.env.NODE_ENV) process.env.NODE_ENV = options._prod ? "production" : "development";
const entry = await loadEntry(options);
const forceUseNode = entry._legacyNode;
const { serve: srvxServe } = forceUseNode ? await import("srvx/node") : await import("srvx");
const { serveStatic } = await import("srvx/static");
const { log } = await import("srvx/log");
const staticDir = resolve(options._dir, options._static);
options._static = existsSync(staticDir) ? staticDir : "";
const server = srvxServe({
error: (error) => {
console.error(error);
return renderError(error);
},
middleware: [
log(),
options._static ? serveStatic({ dir: options._static }) : void 0,
...entry.middleware || []
].filter(Boolean),
...entry
});
globalThis.__srvx__ = server;
await server.ready();
await globalThis.__srvx_listen_cb__?.();
printInfo(entry);
const cleanup = () => {
server.close(true).catch(() => {});
process.exit(0);
};
process.on("SIGINT", () => {
console.log(Colors.gray("\rStopping server..."));
cleanup();
});
process.on("SIGTERM", cleanup);
} catch (error) {
console.error(error);
process.exit(1);
}
}
async function loadEntry(opts) {
try {
if (!opts._entry) for (const entry of defaultEntries) {
for (const ext of defaultExts) {
const entryPath = resolve(opts._dir, `${entry}${ext}`);
if (existsSync(entryPath)) {
opts._entry = entryPath;
break;
}
}
if (opts._entry) break;
}
if (!opts._entry) {
const _error$1 = `No server entry file found.\nPlease specify an entry file or ensure one of the default entries exists (${defaultEntries.join(", ")}).`;
return {
_error: _error$1,
fetch: () => renderError(_error$1, 404, "No Server Entry"),
...opts
};
}
const entryURL = opts._entry.startsWith("file://") ? opts._entry : pathToFileURL(resolve(opts._entry)).href;
const { res: mod, listenHandler } = await interceptListen(() => import(entryURL));
let fetchHandler = mod.fetch || mod.default?.fetch || mod.default?.default?.fetch;
let _legacyNode = false;
if (!fetchHandler) {
const nodeHandler = listenHandler || (typeof mod.default === "function" ? mod.default : void 0);
if (nodeHandler) {
_legacyNode = true;
const { callNodeHandler } = await import("./_chunks/call-LB9MY5Dv.mjs");
fetchHandler = (webReq) => callNodeHandler(nodeHandler, webReq);
}
}
let _error;
if (!fetchHandler) {
_error = `The entry file "${relative(".", opts._entry)}" does not export a valid fetch handler.`;
fetchHandler = () => renderError(_error, 500, "Invalid Entry");
}
return {
...mod,
...mod.default,
...opts,
_error,
_legacyNode,
fetch: fetchHandler
};
} catch (error) {
if (error?.code === "ERR_UNKNOWN_FILE_EXTENSION") {
const message = String(error);
if (/"\.(m|c)?ts"/g.test(message)) console.error(Colors.red(`\nMake sure you're using Node.js v22.18+ or v24+ for TypeScript support (current version: ${process.versions.node})\n\n`));
else if (/"\.(m|c)?tsx"/g.test(message)) console.error(Colors.red(`\nYou need a compatible loader for JSX support (Deno, Bun or srvx --register jiti/register)\n\n`));
}
if (error instanceof Error) Error.captureStackTrace?.(error, serve);
throw error;
}
}
function renderError(error, status = 500, title = "Server Error") {
let html = `
${title}`;
if (options._prod) html += `${title}
Something went wrong while processing your request.
`;
else html += `
${title}
${error instanceof Error ? error.stack || error.message : String(error)}
`;
return new Response(html, {
status,
headers: { "Content-Type": "text/html; charset=utf-8" }
});
}
function printInfo(entry) {
let entryInfo;
if (options._entry) entryInfo = Colors.cyan("./" + relative(".", options._entry));
else entryInfo = Colors.gray(`(create ${Colors.bold(`server.ts`)} to enable)`);
console.log(Colors.gray(`${Colors.bold(Colors.gray("λ"))} Server handler: ${entryInfo}`));
if (options._entry && entry._error) console.error(Colors.red(` ${entry._error}`));
let staticInfo;
if (options._static) staticInfo = Colors.cyan("./" + relative(".", options._static) + "/");
else staticInfo = Colors.gray(`(add ${Colors.bold("public/")} dir to enable)`);
console.log(Colors.gray(`${Colors.bold(Colors.gray("∘"))} Static files: ${staticInfo}`));
}
async function interceptListen(cb) {
const http = process.getBuiltinModule("node:http");
if (!http || !http.Server) {
const res$1 = await cb();
return { res: res$1 };
}
const originalListen = http.Server.prototype.listen;
let res;
let listenHandler;
try {
http.Server.prototype.listen = function(arg1, arg2) {
listenHandler = this._events.request;
if (Array.isArray(listenHandler)) listenHandler = listenHandler[0];
http.Server.prototype.listen = originalListen;
globalThis.__srvx_listen_cb__ = [arg1, arg2].find((arg) => typeof arg === "function");
return new Proxy({}, { get(_, prop) {
const server = globalThis.__srvx__;
return server?.node?.server?.[prop];
} });
};
res = await cb();
} finally {
http.Server.prototype.listen = originalListen;
}
return {
res,
listenHandler
};
}
async function version() {
const version$1 = "0.8.9";
return `srvx ${version$1}\n${runtime()}`;
}
function runtime() {
if (process.versions.bun) return `bun ${process.versions.bun}`;
else if (process.versions.deno) return `deno ${process.versions.deno}`;
else return `node ${process.versions.node}`;
}
function parseArgs$1(args$1) {
const { values, positionals } = parseArgs({
args: args$1,
allowPositionals: true,
options: {
help: {
type: "boolean",
short: "h"
},
version: {
type: "boolean",
short: "v"
},
prod: { type: "boolean" },
port: {
type: "string",
short: "p"
},
host: {
type: "string",
short: "H"
},
static: {
type: "string",
short: "s"
},
import: { type: "string" },
tls: { type: "boolean" },
cert: { type: "string" },
key: { type: "string" }
}
});
const input = positionals[0] || ".";
let dir;
let entry = "";
if (extname(input) === "") dir = resolve(input);
else {
entry = resolve(input);
dir = dirname(entry);
}
if (!existsSync(dir)) {
console.error(Colors.red(`Directory "${dir}" does not exist.\n`));
process.exit(1);
}
return {
_dir: dir,
_entry: entry,
_prod: values.prod ?? process.env.NODE_ENV === "production",
_help: values.help,
_static: values.static || "public",
_version: values.version,
_import: values.import,
port: values.port ? Number.parseInt(values.port, 10) : void 0,
hostname: values.host,
tls: values.tls ? {
cert: values.cert,
key: values.key
} : void 0
};
}
function example() {
const useTs = !options._entry || options._entry.endsWith(".ts");
return `${Colors.bold(Colors.gray("// server.ts"))}
${Colors.magenta("export default")} {
${Colors.cyan("fetch")}(req${useTs ? ": Request" : ""}) {
${Colors.magenta("return")} new Response(${Colors.green("\"Hello, World!\"")});
}
}`;
}
function usage(mainOpts) {
const command = mainOpts.command;
return `
${Colors.cyan(command)} - Start an HTTP server with the specified entry path.
${Colors.bold("USAGE")}
${existsSync(options._entry) ? "" : `\n${example()}\n`}
${Colors.gray("# srvx [options] [entry]")}
${Colors.gray("$")} ${Colors.cyan(command)} ${Colors.gray("./server.ts")} ${Colors.gray("# Start development server")}
${Colors.gray("$")} ${Colors.cyan(command)} --prod ${Colors.gray("# Start production server")}
${Colors.gray("$")} ${Colors.cyan(command)} --port=8080 ${Colors.gray("# Listen on port 8080")}
${Colors.gray("$")} ${Colors.cyan(command)} --host=localhost ${Colors.gray("# Bind to localhost only")}
${Colors.gray("$")} ${Colors.cyan(command)} --import=jiti/register ${Colors.gray(`# Enable ${Colors.url("jiti", "https://github.com/unjs/jiti")} loader`)}
${Colors.gray("$")} ${Colors.cyan(command)} --tls --cert=cert.pem --key=key.pem ${Colors.gray("# Enable TLS (HTTPS/HTTP2)")}
${Colors.bold("ARGUMENTS")}
${Colors.yellow("")} Server entry path to serve.
Default: ${defaultEntries.map((e) => Colors.cyan(e)).join(", ")} ${Colors.gray(`(${defaultExts.join(",")})`)}
${Colors.bold("OPTIONS")}
${Colors.green("-p, --port")} ${Colors.yellow("")} Port to listen on (default: ${Colors.yellow("3000")})
${Colors.green("--host")} ${Colors.yellow("")} Host to bind to (default: all interfaces)
${Colors.green("-s, --static")} ${Colors.yellow("")} Serve static files from the specified directory (default: ${Colors.yellow("public")})
${Colors.green("--prod")} Run in production mode (no watch, no debug)
${Colors.green("--import")} ${Colors.yellow("")} ES module to preload
${Colors.green("--tls")} Enable TLS (HTTPS/HTTP2)
${Colors.green("--cert")} ${Colors.yellow("")} TLS certificate file
${Colors.green("--key")} ${Colors.yellow("")} TLS private key file
${Colors.green("-h, --help")} Show this help message
${Colors.green("-v, --version")} Show server and runtime versions
${Colors.bold("ENVIRONMENT")}
${Colors.green("PORT")} Override port
${Colors.green("HOST")} Override host
${Colors.green("NODE_ENV")} Set to ${Colors.yellow("production")} for production mode.
➤ ${Colors.url("Documentation", mainOpts.docs || "https://srvx.h3.dev")}
➤ ${Colors.url("Report issues", mainOpts.issues || "https://github.com/h3js/srvx/issues")}
`.trim();
}
function setupProcessErrorHandlers() {
process.on("uncaughtException", (error) => {
console.error("Uncaught exception:", error);
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
process.exit(1);
});
}
//#endregion
export { main, usage };