sevok 0.0.2

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 ADDED
@@ -0,0 +1,461 @@
1
+ import { u as loadServerAdapter } from "./core-CmUugTW7.mjs";
2
+ import { t as _color_default } from "./_color-B42q-MpL.mjs";
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { fork } from "node:child_process";
7
+ import { dirname, relative, resolve } from "node:path";
8
+ //#endregion
9
+ //#region src/_meta.ts
10
+ const pkgMeta = {
11
+ name: "sevok",
12
+ version: "0.0.2",
13
+ description: "Composable Server Primitives Across Runtimes"
14
+ };
15
+ //#endregion
16
+ //#region src/cli.ts
17
+ const defaultExts = [
18
+ ".mjs",
19
+ ".js",
20
+ ".mts",
21
+ ".ts"
22
+ ];
23
+ const defaultEntries = [
24
+ "server",
25
+ "server/index",
26
+ "src/server",
27
+ "server/server"
28
+ ];
29
+ /**
30
+ * Resolve and import a user server entry module.
31
+ *
32
+ * The loader can work with an explicit `entry` path or fall back to a list of
33
+ * conventional filenames such as `server.ts` and `src/server.ts`. After the
34
+ * module is imported, plain object default exports are unwrapped, a fetch
35
+ * handler is extracted, and a runtime adapter is selected for the current
36
+ * process.
37
+ */
38
+ async function loadServerEntry(opts) {
39
+ let entry = opts.entry;
40
+ if (entry) {
41
+ entry = resolve(opts.dir || ".", entry);
42
+ if (!existsSync(entry)) return {
43
+ notFound: true,
44
+ adapter: await loadServerAdapter()
45
+ };
46
+ } else {
47
+ for (const defEntry of defaultEntries) {
48
+ for (const defExt of defaultExts) {
49
+ const entryPath = resolve(opts.dir || ".", `${defEntry}${defExt}`);
50
+ if (existsSync(entryPath)) {
51
+ entry = entryPath;
52
+ break;
53
+ }
54
+ }
55
+ if (entry) break;
56
+ }
57
+ if (!entry) return {
58
+ notFound: true,
59
+ adapter: await loadServerAdapter()
60
+ };
61
+ }
62
+ const url = entry.startsWith("file://") ? entry : pathToFileURL(resolve(entry)).href;
63
+ let mod;
64
+ try {
65
+ mod = await import(url);
66
+ if (mod?.default != null && typeof mod.default === "object" && !Array.isArray(mod.default)) mod = mod.default;
67
+ } catch (error) {
68
+ if (error?.code === "ERR_UNKNOWN_FILE_EXTENSION") {
69
+ const message = String(error);
70
+ if (/"\.(m|c)?ts"/g.test(message)) throw new Error(`Make sure you're using Node.js v22.18+ or v24+ for TypeScript support (current version: ${process.versions.node})`, { cause: error });
71
+ else if (/"\.(m|c)?tsx"/g.test(message)) throw new Error(`You need a compatible loader for JSX support (Deno, Bun, or sevok --import jiti/register)`, { cause: error });
72
+ }
73
+ throw error;
74
+ }
75
+ mod = await opts?.onLoad?.(mod) || mod;
76
+ return {
77
+ module: mod,
78
+ url,
79
+ fetch: mod.fetch,
80
+ adapter: await loadServerAdapter()
81
+ };
82
+ }
83
+ /**
84
+ * Main CLI entrypoint.
85
+ *
86
+ * Handles top-level flags, resolves environment files, and decides whether the
87
+ * current process should serve directly or fork a watched child process.
88
+ */
89
+ async function main(mainOpts) {
90
+ const args = mainOpts.args ?? [];
91
+ const cliOpts = parseArgs$1(args);
92
+ if (cliOpts.version) {
93
+ process.stdout.write(versions(mainOpts).join("\n") + "\n");
94
+ process.exit(0);
95
+ }
96
+ if (cliOpts.help) {
97
+ console.log(usage(mainOpts));
98
+ process.exit(cliOpts.help ? 0 : 1);
99
+ }
100
+ if (process.send) return startServer(cliOpts);
101
+ console.log(_color_default.gray([...versions(mainOpts), cliOpts.prod ? "prod" : "dev"].join(" · ")));
102
+ const envFiles = [
103
+ ".env",
104
+ ".env.local",
105
+ cliOpts.prod ? ".env.production" : ".env.development"
106
+ ].filter((f) => existsSync(f));
107
+ if (envFiles.length > 0) console.log(`${_color_default.gray(`Loading environment variables from ${_color_default.magenta(envFiles.join(", "))}`)}`);
108
+ if (cliOpts.prod && !cliOpts.import) {
109
+ for (const envFile of [...envFiles].reverse()) process.loadEnvFile?.(envFile);
110
+ await startServer(cliOpts);
111
+ return;
112
+ }
113
+ const isBun = !!process.versions.bun;
114
+ const isDeno = !!process.versions.deno;
115
+ const isNode = !isBun && !isDeno;
116
+ const runtimeArgs = [];
117
+ runtimeArgs.push(...envFiles.map((f) => `--env-file=${f}`));
118
+ if (!cliOpts.prod) runtimeArgs.push("--watch");
119
+ if (cliOpts.import && (isNode || isBun)) runtimeArgs.push(`--import=${cliOpts.import}`);
120
+ await forkCLI(args, runtimeArgs);
121
+ }
122
+ /**
123
+ * Parse command-line arguments for the `serve` command.
124
+ */
125
+ function parseArgs$1(args) {
126
+ const { values, positionals } = parseArgs({
127
+ args,
128
+ allowPositionals: true,
129
+ options: {
130
+ help: {
131
+ type: "boolean",
132
+ short: "h"
133
+ },
134
+ version: {
135
+ type: "boolean",
136
+ short: "v"
137
+ },
138
+ dir: { type: "string" },
139
+ entry: { type: "string" },
140
+ host: { type: "string" },
141
+ hostname: { type: "string" },
142
+ tls: { type: "boolean" },
143
+ url: { type: "string" },
144
+ prod: { type: "boolean" },
145
+ port: {
146
+ type: "string",
147
+ short: "p"
148
+ },
149
+ static: {
150
+ type: "string",
151
+ short: "s"
152
+ },
153
+ import: { type: "string" },
154
+ cert: { type: "string" },
155
+ key: { type: "string" }
156
+ }
157
+ });
158
+ const maybeEntryOrDir = positionals[0];
159
+ if (maybeEntryOrDir) {
160
+ if (values.entry || values.dir) throw new Error("Cannot specify entry or dir as positional argument when --entry or --dir is used!");
161
+ if (statSync(maybeEntryOrDir).isDirectory()) values.dir = maybeEntryOrDir;
162
+ else values.entry = maybeEntryOrDir;
163
+ }
164
+ return values;
165
+ }
166
+ /**
167
+ * Prepare process-level error handlers and start serving.
168
+ */
169
+ async function startServer(cliOpts) {
170
+ setupProcessErrorHandlers();
171
+ await cliServe(cliOpts);
172
+ }
173
+ /**
174
+ * Spawn a child CLI process for watch mode or runtime-preload scenarios.
175
+ *
176
+ * The parent process supervises the child and forwards selected termination
177
+ * paths so the child does not outlive the CLI session.
178
+ */
179
+ async function forkCLI(args, runtimeArgs) {
180
+ const child = fork(fileURLToPath(globalThis.__SEVOK_BIN__ || new URL("../bin/sevok.mjs", import.meta.url)), [...args], { execArgv: [...process.execArgv, ...runtimeArgs].filter(Boolean) });
181
+ child.on("error", (error) => {
182
+ console.error("Error in child process:", error);
183
+ process.exit(1);
184
+ });
185
+ child.on("exit", (code) => {
186
+ if (code !== 0) {
187
+ console.error(`Child process exited with code ${code}`);
188
+ process.exit(code);
189
+ }
190
+ });
191
+ child.on("message", (msg) => {
192
+ if (msg && msg.error === "no-entry") {
193
+ console.error("\n" + _color_default.red(NO_ENTRY_ERROR) + "\n");
194
+ process.exit(3);
195
+ }
196
+ });
197
+ let cleanupCalled = false;
198
+ const cleanup = (signal, exitCode) => {
199
+ if (cleanupCalled) return;
200
+ cleanupCalled = true;
201
+ try {
202
+ child.kill(signal || "SIGTERM");
203
+ } catch (error) {
204
+ console.error("Error killing child process:", error);
205
+ }
206
+ if (exitCode !== void 0) process.exit(exitCode);
207
+ };
208
+ process.on("exit", () => cleanup("SIGTERM"));
209
+ process.on("SIGTERM", () => cleanup("SIGTERM", 143));
210
+ if (args.includes("--watch")) process.on("SIGINT", () => cleanup("SIGINT", 130));
211
+ }
212
+ /**
213
+ * Convert uncaught process-level failures into a clear non-zero exit.
214
+ */
215
+ function setupProcessErrorHandlers() {
216
+ process.on("uncaughtException", (error) => {
217
+ console.error("Uncaught exception:", error);
218
+ process.exit(1);
219
+ });
220
+ process.on("unhandledRejection", (reason) => {
221
+ console.error("Unhandled rejection:", reason);
222
+ process.exit(1);
223
+ });
224
+ }
225
+ /**
226
+ * Build the version banner printed by the CLI.
227
+ */
228
+ function versions(mainOpts) {
229
+ const versions = [];
230
+ if (mainOpts.meta?.name) versions.push(`${mainOpts.meta.name} ${mainOpts.meta.version || ""}`.trim());
231
+ versions.push(`${pkgMeta.name} ${pkgMeta.version}`);
232
+ versions.push(runtime());
233
+ return versions;
234
+ }
235
+ /**
236
+ * Detect the current JavaScript runtime for display purposes.
237
+ */
238
+ function runtime() {
239
+ if (process.versions.bun) return `bun ${process.versions.bun}`;
240
+ else if (process.versions.deno) return `deno ${process.versions.deno}`;
241
+ else return `node ${process.versions.node}`;
242
+ }
243
+ /**
244
+ * Render the CLI help output.
245
+ */
246
+ function usage(mainOpts) {
247
+ const name = mainOpts.meta?.name || pkgMeta.name.split("/").pop();
248
+ const ver = mainOpts.meta?.version || pkgMeta.version;
249
+ const desc = mainOpts.meta?.description || pkgMeta.description;
250
+ const formatSection = (rows) => {
251
+ const labelWidth = rows.reduce((width, row) => Math.max(width, row.label.length), 0);
252
+ return rows.map((row) => ` ${row.renderLabel().padEnd(labelWidth + (row.renderLabel().length - row.label.length))} ${row.description}`).join("\n");
253
+ };
254
+ const usageRows = [{
255
+ label: `${name} <file>`,
256
+ renderLabel: () => `${_color_default.green(name)} ${_color_default.yellow("<file>")}`,
257
+ description: "Use a server entry file as a positional alias for --entry"
258
+ }, {
259
+ label: `${name} <dir>`,
260
+ renderLabel: () => `${_color_default.green(name)} ${_color_default.yellow("<dir>")}`,
261
+ description: "Use a working directory as a positional alias for --dir"
262
+ }];
263
+ const optionRows = [
264
+ {
265
+ label: "--entry <file>",
266
+ renderLabel: () => `${_color_default.green("--entry")} ${_color_default.yellow("<file>")}`,
267
+ description: "Server entry file to use"
268
+ },
269
+ {
270
+ label: "--dir <dir>",
271
+ renderLabel: () => `${_color_default.green("--dir")} ${_color_default.yellow("<dir>")}`,
272
+ description: "Working directory for resolving entry file"
273
+ },
274
+ {
275
+ label: "-h, --help",
276
+ renderLabel: () => _color_default.green("-h, --help"),
277
+ description: "Show this help message"
278
+ },
279
+ {
280
+ label: "-v, --version",
281
+ renderLabel: () => _color_default.green("-v, --version"),
282
+ description: "Show server and runtime versions"
283
+ },
284
+ {
285
+ label: "-p, --port <port>",
286
+ renderLabel: () => `${_color_default.green("-p, --port")} ${_color_default.yellow("<port>")}`,
287
+ description: `Port to listen on (default: ${_color_default.yellow("3000")})`
288
+ },
289
+ {
290
+ label: "--host, --hostname <host>",
291
+ renderLabel: () => `${_color_default.green("--host, --hostname")} ${_color_default.yellow("<host>")}`,
292
+ description: "Host to bind to (default: all interfaces)"
293
+ },
294
+ {
295
+ label: "-s, --static <dir>",
296
+ renderLabel: () => `${_color_default.green("-s, --static")} ${_color_default.yellow("<dir>")}`,
297
+ description: `Serve static files from the specified directory (default: ${_color_default.yellow("public")})`
298
+ },
299
+ {
300
+ label: "--prod",
301
+ renderLabel: () => _color_default.green("--prod"),
302
+ description: "Disable watch mode and use production env defaults"
303
+ },
304
+ {
305
+ label: "--import <loader>",
306
+ renderLabel: () => `${_color_default.green("--import")} ${_color_default.yellow("<loader>")}`,
307
+ description: "ES module to preload (Node.js / Bun only)"
308
+ },
309
+ {
310
+ label: "--tls",
311
+ renderLabel: () => _color_default.green("--tls"),
312
+ description: "Enable TLS (HTTPS/HTTP2)"
313
+ },
314
+ {
315
+ label: "--cert <file>",
316
+ renderLabel: () => `${_color_default.green("--cert")} ${_color_default.yellow("<file>")}`,
317
+ description: "TLS certificate file"
318
+ },
319
+ {
320
+ label: "--key <file>",
321
+ renderLabel: () => `${_color_default.green("--key")} ${_color_default.yellow("<file>")}`,
322
+ description: "TLS private key file"
323
+ }
324
+ ];
325
+ const environmentRows = [
326
+ {
327
+ label: "PORT",
328
+ renderLabel: () => _color_default.green("PORT"),
329
+ description: "Override port"
330
+ },
331
+ {
332
+ label: "HOST",
333
+ renderLabel: () => _color_default.green("HOST"),
334
+ description: "Override host"
335
+ },
336
+ {
337
+ label: "NODE_ENV",
338
+ renderLabel: () => _color_default.green("NODE_ENV"),
339
+ description: `Defaults to ${_color_default.yellow("development")} or ${_color_default.yellow("production")} based on --prod.`
340
+ }
341
+ ];
342
+ return `
343
+ ${_color_default.cyan(name)}${_color_default.gray(`${ver ? ` ${ver}` : ""} ${desc ? `- ${desc}` : ""}`)}
344
+
345
+ ${_color_default.bold("USAGE")}
346
+
347
+ ${formatSection(usageRows)}
348
+
349
+ ${_color_default.bold("OPTIONS")}
350
+
351
+ ${formatSection(optionRows)}
352
+
353
+ ${_color_default.bold("ENVIRONMENT")}
354
+
355
+ ${formatSection(environmentRows)}
356
+
357
+ ${mainOpts.usage?.docs ? `➤ ${_color_default.url("Documentation", mainOpts.usage.docs)}` : ""}
358
+ ${mainOpts.usage?.issues ? `➤ ${_color_default.url("Report issues", mainOpts.usage.issues)}` : ""}
359
+ `.trim();
360
+ }
361
+ const NO_ENTRY_ERROR = "No server entry or public directory found";
362
+ /**
363
+ * Resolve the user entry module, attach default middleware, and start the
364
+ * runtime adapter-backed server instance.
365
+ */
366
+ async function cliServe(cliOpts) {
367
+ try {
368
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = cliOpts.prod ? "production" : "development";
369
+ let server;
370
+ const loaded = await loadServerEntry({
371
+ entry: cliOpts.entry,
372
+ dir: cliOpts.dir
373
+ });
374
+ const { serve } = await import("./core-CmUugTW7.mjs").then((n) => n.o);
375
+ const { serveStatic } = await import("./static.mjs");
376
+ const { log } = await import("./log.mjs");
377
+ const staticDir = resolve(cliOpts.dir || (loaded.url ? dirname(fileURLToPath(loaded.url)) : "."), cliOpts.static || "public");
378
+ const staticExplicitlySet = !!cliOpts.static;
379
+ const staticExists = existsSync(staticDir);
380
+ if (staticExplicitlySet && !staticExists) console.warn(_color_default.yellow(`Warning: Static directory not found: ${staticDir}`));
381
+ cliOpts.static = staticExists ? staticDir : "";
382
+ if (loaded.notFound && !cliOpts.static) {
383
+ process.send?.({ error: "no-entry" });
384
+ throw new Error(NO_ENTRY_ERROR, { cause: cliOpts });
385
+ }
386
+ const serverOptions = {
387
+ ...loaded.module?.default,
388
+ default: void 0,
389
+ ...loaded.module
390
+ };
391
+ printInfo(cliOpts, loaded);
392
+ server = serve({
393
+ ...serverOptions,
394
+ gracefulShutdown: !!cliOpts.prod,
395
+ port: cliOpts.port ?? serverOptions.port,
396
+ hostname: cliOpts.hostname ?? cliOpts.host ?? serverOptions.hostname,
397
+ tls: cliOpts.tls ? {
398
+ cert: cliOpts.cert,
399
+ key: cliOpts.key
400
+ } : void 0,
401
+ error: (error) => {
402
+ console.error(error);
403
+ return renderError(cliOpts, error);
404
+ },
405
+ fetch: loaded.fetch || (() => renderError(cliOpts, loaded.notFound ? "Server Entry Not Found" : "No Fetch Handler Exported", 501)),
406
+ middleware: [
407
+ log(),
408
+ cliOpts.static ? serveStatic({ dir: cliOpts.static }) : void 0,
409
+ ...serverOptions.middleware || []
410
+ ].filter(Boolean),
411
+ adapter: loaded.adapter
412
+ });
413
+ await server?.ready();
414
+ } catch (error) {
415
+ console.error(error);
416
+ process.exit(1);
417
+ }
418
+ }
419
+ /**
420
+ * Render an HTML error page for CLI-served requests.
421
+ *
422
+ * Production mode returns a minimal message while development mode includes the
423
+ * full error details for debugging.
424
+ */
425
+ function renderError(cliOpts, error, status = 500, title = "Server Error") {
426
+ let html = `<!DOCTYPE html><html><head><title>${title}</title></head><body>`;
427
+ if (cliOpts.prod) html += `<h1>${title}</h1><p>Something went wrong while processing your request.</p>`;
428
+ else html += `
429
+ <style>
430
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; color: #333; }
431
+ h1 { color: #dc3545; }
432
+ pre { background: #fff; padding: 10px; border-radius: 5px; overflow: auto; }
433
+ code { font-family: monospace; }
434
+ #error { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; }
435
+ </style>
436
+ <div id="error"><h1>${title}</h1><pre>${error instanceof Error ? error.stack || error.message : String(error)}</pre></div>
437
+ `;
438
+ return new Response(html, {
439
+ status,
440
+ headers: { "Content-Type": "text/html; charset=utf-8" }
441
+ });
442
+ }
443
+ /**
444
+ * Print resolved entry and static directory information before the server
445
+ * starts listening.
446
+ */
447
+ function printInfo(cliOpts, loaded) {
448
+ let entryInfo;
449
+ if (loaded.notFound) entryInfo = _color_default.gray(`(create ${_color_default.bold(`server.ts`)})`);
450
+ else entryInfo = loaded.fetch ? _color_default.cyan("./" + relative(".", fileURLToPath(loaded.url))) : _color_default.red(`No fetch handler exported from ${loaded.url}`);
451
+ console.log(_color_default.gray(`${_color_default.bold(_color_default.gray("◆"))} Server handler: ${entryInfo}`));
452
+ let staticInfo;
453
+ if (cliOpts.static) {
454
+ const relPath = relative(".", cliOpts.static);
455
+ staticInfo = _color_default.cyan(relPath ? "./" + relPath + "/" : "./");
456
+ } else staticInfo = _color_default.gray(`(create ${_color_default.bold("public/")} dir)`);
457
+ console.log(_color_default.gray(`${_color_default.bold(_color_default.gray("◇"))} Static files: ${staticInfo}`));
458
+ console.log("");
459
+ }
460
+ //#endregion
461
+ export { NO_ENTRY_ERROR, cliServe, defaultEntries, defaultExts, loadServerEntry, main, usage };