feat: init
This commit is contained in:
421
node_modules/srvx/dist/cli.mjs
generated
vendored
Normal file
421
node_modules/srvx/dist/cli.mjs
generated
vendored
Normal file
@@ -0,0 +1,421 @@
|
||||
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/_utils.mjs";
|
||||
import { loadServerEntry } from "./loader.mjs";
|
||||
import { parseArgs } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fork } from "node:child_process";
|
||||
import { createReadStream, existsSync, statSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
const NO_ENTRY_ERROR = "No server entry or public directory found";
|
||||
async function cliServe(cliOpts) {
|
||||
try {
|
||||
if (!process.env.NODE_ENV) process.env.NODE_ENV = cliOpts.prod ? "production" : "development";
|
||||
const loaded = await loadServerEntry({
|
||||
entry: cliOpts.entry,
|
||||
dir: cliOpts.dir
|
||||
});
|
||||
const { serve: srvxServe } = loaded.nodeCompat ? await import("srvx/node") : await import("srvx");
|
||||
const { serveStatic } = await import("srvx/static");
|
||||
const { log } = await import("srvx/log");
|
||||
const staticDir = resolve(cliOpts.dir || (loaded.url ? dirname(fileURLToPath(loaded.url)) : "."), cliOpts.static || "public");
|
||||
cliOpts.static = existsSync(staticDir) ? staticDir : "";
|
||||
if (loaded.notFound && !cliOpts.static) {
|
||||
process.send?.({ error: "no-entry" });
|
||||
throw new Error(NO_ENTRY_ERROR, { cause: cliOpts });
|
||||
}
|
||||
const serverOptions = {
|
||||
...loaded.module?.default,
|
||||
default: void 0,
|
||||
...loaded.module
|
||||
};
|
||||
printInfo(cliOpts, loaded);
|
||||
await (globalThis.__srvx__ = srvxServe({
|
||||
...serverOptions,
|
||||
gracefulShutdown: !!cliOpts.prod,
|
||||
port: cliOpts.port ?? serverOptions.port,
|
||||
hostname: cliOpts.hostname ?? cliOpts.host ?? serverOptions.hostname,
|
||||
tls: cliOpts.tls ? {
|
||||
cert: cliOpts.cert,
|
||||
key: cliOpts.key
|
||||
} : void 0,
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
return renderError(cliOpts, error);
|
||||
},
|
||||
fetch: loaded.fetch || (() => renderError(cliOpts, loaded.notFound ? "Server Entry Not Found" : "No Fetch Handler Exported", 501)),
|
||||
middleware: [
|
||||
log(),
|
||||
cliOpts.static ? serveStatic({ dir: cliOpts.static }) : void 0,
|
||||
...serverOptions.middleware || []
|
||||
].filter(Boolean)
|
||||
})).ready();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
function renderError(cliOpts, error, status = 500, title = "Server Error") {
|
||||
let html = `<!DOCTYPE html><html><head><title>${title}</title></head><body>`;
|
||||
if (cliOpts.prod) html += `<h1>${title}</h1><p>Something went wrong while processing your request.</p>`;
|
||||
else html += `
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; color: #333; }
|
||||
h1 { color: #dc3545; }
|
||||
pre { background: #fff; padding: 10px; border-radius: 5px; overflow: auto; }
|
||||
code { font-family: monospace; }
|
||||
#error { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; }
|
||||
</style>
|
||||
<div id="error"><h1>${title}</h1><pre>${error instanceof Error ? error.stack || error.message : String(error)}</pre></div>
|
||||
`;
|
||||
return new Response(html, {
|
||||
status,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" }
|
||||
});
|
||||
}
|
||||
function printInfo(cliOpts, loaded) {
|
||||
let entryInfo;
|
||||
if (loaded.notFound) entryInfo = gray(`(create ${bold(`server.ts`)})`);
|
||||
else entryInfo = loaded.fetch ? cyan("./" + relative(".", fileURLToPath(loaded.url))) : red(`No fetch handler exported from ${loaded.url}`);
|
||||
console.log(gray(`${bold(gray("◆"))} Server handler: ${entryInfo}`));
|
||||
let staticInfo;
|
||||
if (cliOpts.static) staticInfo = cyan("./" + relative(".", cliOpts.static) + "/");
|
||||
else staticInfo = gray(`(create ${bold("public/")} dir)`);
|
||||
console.log(gray(`${bold(gray("◇"))} Static files: ${staticInfo}`));
|
||||
console.log("");
|
||||
}
|
||||
async function cliFetch(cliOpts) {
|
||||
const stdin = cliOpts.stdin || process.stdin;
|
||||
const stdout = cliOpts.stdout || process.stdout;
|
||||
const stderr = cliOpts.stderr || process.stderr;
|
||||
let fetchHandler = globalThis.fetch;
|
||||
let inputURL = cliOpts.url || "/";
|
||||
if (inputURL[0] === "/") {
|
||||
const loaded = await loadServerEntry({
|
||||
dir: cliOpts.dir,
|
||||
entry: cliOpts.entry,
|
||||
...cliOpts?.loader
|
||||
});
|
||||
if (cliOpts.verbose && loaded.url) {
|
||||
stderr.write(`* Entry: ${fileURLToPath(loaded.url)}\n`);
|
||||
if (loaded.nodeCompat) stderr.write(`* Using node compat mode\n`);
|
||||
}
|
||||
if (loaded.notFound) throw new Error(`Server entry file not found in ${resolve(cliOpts.dir || ".")}`, { cause: {
|
||||
dir: cliOpts.dir || process.cwd(),
|
||||
entry: cliOpts.entry || void 0
|
||||
} });
|
||||
else if (!loaded.fetch) throw new Error("No fetch handler exported", { cause: {
|
||||
dir: cliOpts.dir || process.cwd(),
|
||||
entry: cliOpts.entry || void 0,
|
||||
loaded
|
||||
} });
|
||||
fetchHandler = loaded.fetch;
|
||||
} else {
|
||||
stderr.write(`* Fetching remote URL: ${inputURL}\n`);
|
||||
if (!URL?.canParse(inputURL)) inputURL = `http${cliOpts.tls ? "s" : ""}://${inputURL}`;
|
||||
fetchHandler = globalThis.fetch;
|
||||
}
|
||||
const headers = new Headers();
|
||||
if (cliOpts.header) for (const header of cliOpts.header) {
|
||||
const colonIndex = header.indexOf(":");
|
||||
if (colonIndex > 0) {
|
||||
const name = header.slice(0, colonIndex).trim();
|
||||
const value = header.slice(colonIndex + 1).trim();
|
||||
headers.append(name, value);
|
||||
}
|
||||
}
|
||||
if (!headers.has("User-Agent")) headers.set("User-Agent", "srvx (curl)");
|
||||
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");
|
||||
let body;
|
||||
if (cliOpts.data !== void 0) if (cliOpts.data === "@-") body = new ReadableStream({ async start(controller) {
|
||||
for await (const chunk of stdin) controller.enqueue(chunk);
|
||||
controller.close();
|
||||
} });
|
||||
else if (cliOpts.data.startsWith("@")) body = Readable.toWeb(createReadStream(cliOpts.data.slice(1)));
|
||||
else body = cliOpts.data;
|
||||
const method = cliOpts.method || (body === void 0 ? "GET" : "POST");
|
||||
const url = new URL(inputURL, `http${cliOpts.tls ? "s" : ""}://${cliOpts.host || cliOpts.hostname || "localhost"}`);
|
||||
const req = new Request(url, {
|
||||
method,
|
||||
headers,
|
||||
body
|
||||
});
|
||||
if (cliOpts.verbose) {
|
||||
const parsedUrl = new URL(url);
|
||||
stderr.write(`> ${method} ${parsedUrl.pathname}${parsedUrl.search} HTTP/1.1\n`);
|
||||
stderr.write(`> Host: ${parsedUrl.host}\n`);
|
||||
for (const [name, value] of headers) stderr.write(`> ${name}: ${value}\n`);
|
||||
stderr.write(">\n");
|
||||
}
|
||||
const res = await fetchHandler(req);
|
||||
if (cliOpts.verbose) {
|
||||
stderr.write(`< HTTP/1.1 ${res.status} ${res.statusText}\n`);
|
||||
for (const [name, value] of res.headers) stderr.write(`< ${name}: ${value}\n`);
|
||||
stderr.write("<\n");
|
||||
}
|
||||
if (res.body) {
|
||||
const { isBinary, encoding } = getResponseFormat(res);
|
||||
if (isBinary) for await (const chunk of res.body) stdout.write(chunk);
|
||||
else {
|
||||
const decoder = new TextDecoder(encoding);
|
||||
for await (const chunk of res.body) stdout.write(decoder.decode(chunk, { stream: true }));
|
||||
const remaining = decoder.decode();
|
||||
if (remaining) stdout.write(remaining);
|
||||
if (stdout.isTTY) stdout.write("\n");
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function getResponseFormat(res) {
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
return {
|
||||
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"),
|
||||
encoding: contentType.includes("charset=") ? contentType.split("charset=")[1].split(";")[0].trim() : "utf8"
|
||||
};
|
||||
}
|
||||
const srvxMeta = {
|
||||
name: "srvx",
|
||||
version: "0.11.3",
|
||||
description: "Universal Server."
|
||||
};
|
||||
function usage(mainOpts) {
|
||||
const command = mainOpts.usage?.command || "srvx";
|
||||
const name = mainOpts.meta?.name || srvxMeta.name;
|
||||
const ver = mainOpts.meta?.version || srvxMeta.version;
|
||||
const desc = mainOpts.meta?.description || srvxMeta.description;
|
||||
return `
|
||||
${cyan(name)}${gray(`${ver ? ` ${ver}` : ""} ${desc ? `- ${desc}` : ""}`)}
|
||||
|
||||
${bold("SERVE MODE")}
|
||||
|
||||
${bold(green(`# ${command} serve [options]`))}
|
||||
${gray("$")} ${cyan(command)} serve --entry ${gray("./server.ts")} ${gray("# Start development server")}
|
||||
${gray("$")} ${cyan(command)} serve --prod ${gray("# Start production server")}
|
||||
${gray("$")} ${cyan(command)} serve --port=8080 ${gray("# Listen on port 8080")}
|
||||
${gray("$")} ${cyan(command)} serve --host=localhost ${gray("# Bind to localhost only")}
|
||||
${gray("$")} ${cyan(command)} serve --import=jiti/register ${gray(`# Enable ${url("jiti", "https://github.com/unjs/jiti")} loader`)}
|
||||
${gray("$")} ${cyan(command)} serve --tls --cert=cert.pem --key=key.pem ${gray("# Enable TLS (HTTPS/HTTP2)")}
|
||||
|
||||
${bold("FETCH MODE")}
|
||||
|
||||
${bold(green(`# ${command} fetch|curl [options] [url]`))}
|
||||
${gray("$")} ${cyan(command)} fetch ${gray("# Fetch from default entry")}
|
||||
${gray("$")} ${cyan(command)} fetch /api/users ${gray("# Fetch a specific URL/path")}
|
||||
${gray("$")} ${cyan(command)} fetch --entry ./server.ts /api/users ${gray("# Fetch using a specific entry")}
|
||||
${gray("$")} ${cyan(command)} fetch -X POST /api/users ${gray("# POST request")}
|
||||
${gray("$")} ${cyan(command)} fetch -H "Content-Type: application/json" /api ${gray("# With headers")}
|
||||
${gray("$")} ${cyan(command)} fetch -d '{"name":"foo"}' /api ${gray("# With request body")}
|
||||
${gray("$")} ${cyan(command)} fetch -v /api/users ${gray("# Verbose output (show headers)")}
|
||||
${gray("$")} echo '{"name":"foo"}' | ${cyan(command)} fetch -d @- /api ${gray("# Body from stdin")}
|
||||
|
||||
${bold("COMMON OPTIONS")}
|
||||
|
||||
${green("--entry")} ${yellow("<file>")} Server entry file to use
|
||||
${green("--dir")} ${yellow("<dir>")} Working directory for resolving entry file
|
||||
${green("-h, --help")} Show this help message
|
||||
${green("--version")} Show server and runtime versions
|
||||
|
||||
${bold("SERVE OPTIONS")}
|
||||
|
||||
${green("-p, --port")} ${yellow("<port>")} Port to listen on (default: ${yellow("3000")})
|
||||
${green("--host")} ${yellow("<host>")} Host to bind to (default: all interfaces)
|
||||
${green("-s, --static")} ${yellow("<dir>")} Serve static files from the specified directory (default: ${yellow("public")})
|
||||
${green("--prod")} Run in production mode (no watch, no debug)
|
||||
${green("--import")} ${yellow("<loader>")} ES module to preload
|
||||
${green("--tls")} Enable TLS (HTTPS/HTTP2)
|
||||
${green("--cert")} ${yellow("<file>")} TLS certificate file
|
||||
${green("--key")} ${yellow("<file>")} TLS private key file
|
||||
|
||||
${bold("FETCH OPTIONS")}
|
||||
|
||||
${green("-X, --request")} ${yellow("<method>")} HTTP method (default: ${yellow("GET")}, or ${yellow("POST")} if body is provided)
|
||||
${green("-H, --header")} ${yellow("<header>")} Add header (format: "Name: Value", can be used multiple times)
|
||||
${green("-d, --data")} ${yellow("<data>")} Request body (use ${yellow("@-")} for stdin, ${yellow("@file")} for file)
|
||||
${green("-v, --verbose")} Show request and response headers
|
||||
|
||||
${bold("ENVIRONMENT")}
|
||||
|
||||
${green("PORT")} Override port
|
||||
${green("HOST")} Override host
|
||||
${green("NODE_ENV")} Set to ${yellow("production")} for production mode.
|
||||
|
||||
${mainOpts.usage?.docs ? `➤ ${url("Documentation", mainOpts.usage.docs)}` : ""}
|
||||
${mainOpts.usage?.issues ? `➤ ${url("Report issues", mainOpts.usage.issues)}` : ""}
|
||||
`.trim();
|
||||
}
|
||||
async function main(mainOpts) {
|
||||
const args = process.argv.slice(2);
|
||||
const cliOpts = parseArgs$1(args);
|
||||
if (cliOpts.version) {
|
||||
process.stdout.write(versions(mainOpts).join("\n") + "\n");
|
||||
process.exit(0);
|
||||
}
|
||||
if (cliOpts.help) {
|
||||
console.log(usage(mainOpts));
|
||||
process.exit(cliOpts.help ? 0 : 1);
|
||||
}
|
||||
if (cliOpts.mode === "fetch") try {
|
||||
const res = await cliFetch(cliOpts);
|
||||
process.exit(res.ok ? 0 : 22);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (process.send) return startServer(cliOpts);
|
||||
console.log(gray([...versions(mainOpts), cliOpts.prod ? "prod" : "dev"].join(" · ")));
|
||||
const envFiles = [".env", cliOpts.prod ? ".env.production" : ".env.local"].filter((f) => existsSync(f));
|
||||
if (envFiles.length > 0) console.log(`${gray(`Loading environment variables from ${magenta(envFiles.join(", "))}`)}`);
|
||||
if (cliOpts.prod && !cliOpts.import) {
|
||||
for (const envFile of [...envFiles].reverse()) process.loadEnvFile?.(envFile);
|
||||
await startServer(cliOpts);
|
||||
return;
|
||||
}
|
||||
const isBun = !!process.versions.bun;
|
||||
const isDeno = !!process.versions.deno;
|
||||
const isNode = !isBun && !isDeno;
|
||||
const runtimeArgs = [];
|
||||
runtimeArgs.push(...envFiles.map((f) => `--env-file=${f}`));
|
||||
if (!cliOpts.prod) runtimeArgs.push("--watch");
|
||||
if (cliOpts.import && (isNode || isBun)) runtimeArgs.push(`--import=${cliOpts.import}`);
|
||||
await forkCLI(args, runtimeArgs);
|
||||
}
|
||||
function parseArgs$1(args) {
|
||||
const pArg0 = args.find((a) => !a.startsWith("-"));
|
||||
const mode = pArg0 === "fetch" || pArg0 === "curl" ? "fetch" : "serve";
|
||||
const commonArgs = {
|
||||
help: { type: "boolean" },
|
||||
version: { type: "boolean" },
|
||||
dir: { type: "string" },
|
||||
entry: { type: "string" },
|
||||
host: { type: "string" },
|
||||
hostname: { type: "string" },
|
||||
tls: { type: "boolean" }
|
||||
};
|
||||
if (mode === "serve") {
|
||||
const { values, positionals } = parseArgs({
|
||||
args,
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
...commonArgs,
|
||||
url: { type: "string" },
|
||||
prod: { type: "boolean" },
|
||||
port: {
|
||||
type: "string",
|
||||
short: "p"
|
||||
},
|
||||
static: {
|
||||
type: "string",
|
||||
short: "s"
|
||||
},
|
||||
import: { type: "string" },
|
||||
cert: { type: "string" },
|
||||
key: { type: "string" }
|
||||
}
|
||||
});
|
||||
if (positionals[0] === "serve") positionals.shift();
|
||||
const maybeEntryOrDir = positionals[0];
|
||||
if (maybeEntryOrDir) {
|
||||
if (values.entry || values.dir) throw new Error("Cannot specify entry or dir as positional argument when --entry or --dir is used!");
|
||||
if (statSync(maybeEntryOrDir).isDirectory()) values.dir = maybeEntryOrDir;
|
||||
else values.entry = maybeEntryOrDir;
|
||||
}
|
||||
return {
|
||||
mode,
|
||||
...values
|
||||
};
|
||||
}
|
||||
const { values, positionals } = parseArgs({
|
||||
args,
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
...commonArgs,
|
||||
url: { type: "string" },
|
||||
method: {
|
||||
type: "string",
|
||||
short: "X"
|
||||
},
|
||||
request: { type: "string" },
|
||||
header: {
|
||||
type: "string",
|
||||
multiple: true,
|
||||
short: "H"
|
||||
},
|
||||
verbose: {
|
||||
type: "boolean",
|
||||
short: "v"
|
||||
},
|
||||
data: {
|
||||
type: "string",
|
||||
short: "d"
|
||||
}
|
||||
}
|
||||
});
|
||||
if (positionals[0] === "fetch" || positionals[0] === "curl") positionals.shift();
|
||||
const method = values.method || values.request;
|
||||
const url = values.url || positionals[0] || "/";
|
||||
return {
|
||||
mode,
|
||||
...values,
|
||||
url,
|
||||
method
|
||||
};
|
||||
}
|
||||
async function startServer(cliOpts) {
|
||||
setupProcessErrorHandlers();
|
||||
await cliServe(cliOpts);
|
||||
}
|
||||
async function forkCLI(args, runtimeArgs) {
|
||||
const child = fork(fileURLToPath(globalThis.__SRVX_BIN__ || new URL("../bin/srvx.mjs", 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);
|
||||
}
|
||||
});
|
||||
child.on("message", (msg) => {
|
||||
if (msg && msg.error === "no-entry") {
|
||||
console.error("\n" + red(NO_ENTRY_ERROR) + "\n");
|
||||
process.exit(3);
|
||||
}
|
||||
});
|
||||
let cleanupCalled = false;
|
||||
const cleanup = (signal, exitCode) => {
|
||||
if (cleanupCalled) return;
|
||||
cleanupCalled = true;
|
||||
try {
|
||||
child.kill(signal || "SIGTERM");
|
||||
} catch (error) {
|
||||
console.error("Error killing child process:", error);
|
||||
}
|
||||
if (exitCode !== void 0) process.exit(exitCode);
|
||||
};
|
||||
process.on("exit", () => cleanup("SIGTERM"));
|
||||
process.on("SIGTERM", () => cleanup("SIGTERM", 143));
|
||||
if (args.includes("--watch")) process.on("SIGINT", () => cleanup("SIGINT", 130));
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
function versions(mainOpts) {
|
||||
const versions = [];
|
||||
if (mainOpts.meta?.name) versions.push(`${mainOpts.meta.name} ${mainOpts.meta.version || ""}`.trim());
|
||||
versions.push(`${srvxMeta.name} ${srvxMeta.version}`);
|
||||
versions.push(runtime());
|
||||
return versions;
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
export { cliFetch, main };
|
||||
Reference in New Issue
Block a user