-
-
Notifications
You must be signed in to change notification settings - Fork 52
fix(cli): stabilization batch + CLI test suite #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -32,11 +32,20 @@ export async function cliServe(cliOpts: CLIOptions): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { serveStatic } = await import("srvx/static"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { log } = await import("srvx/log"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // F43: an explicit `--static` pointing at a missing dir must error; the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // implicit `public` default may stay silent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const explicitStatic = !!cliOpts.static; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const staticDir = resolve( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cliOpts.dir || (loaded.url ? dirname(fileURLToPath(loaded.url)) : "."), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cliOpts.static || "public", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cliOpts.static = existsSync(staticDir) ? staticDir : ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existsSync(staticDir)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cliOpts.static = staticDir; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (explicitStatic) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`--static directory not found: ${staticDir}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cliOpts.static = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🐛 Suggested fix- if (existsSync(staticDir)) {
+ if (existsSync(staticDir) && statSync(staticDir).isDirectory()) {
cliOpts.static = staticDir;
} else if (explicitStatic) {
throw new Error(`--static directory not found: ${staticDir}`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (loaded.notFound && !cliOpts.static) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.send?.({ error: "no-entry" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,13 +58,23 @@ export async function cliServe(cliOpts: CLIOptions): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...loaded.module, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } as Partial<ServerOptions>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // F42: only override the entry module's `tls` when CLI flags actually supply | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TLS. A bare `--tls` (no cert/key) must error rather than silently downgrade. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let tls = serverOptions.tls; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cliOpts.tls) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!cliOpts.cert || !cliOpts.key) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("--tls requires both --cert and --key."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tls = { cert: cliOpts.cert, key: cliOpts.key }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| printInfo(cliOpts, loaded); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server = 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 } : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tls, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return renderError(cliOpts, error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -91,9 +110,10 @@ function renderError( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status = 500, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = "Server Error", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Response { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let html = `<!DOCTYPE html><html><head><title>${title}</title></head><body>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const safeTitle = escapeHtml(title); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let html = `<!DOCTYPE html><html><head><title>${safeTitle}</title></head><body>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cliOpts.prod) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| html += `<h1>${title}</h1><p>Something went wrong while processing your request.</p>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| html += `<h1>${safeTitle}</h1><p>Something went wrong while processing your request.</p>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| html += /* html */ ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <style> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -103,7 +123,9 @@ function renderError( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="error"><h1>${safeTitle}</h1><pre>${escapeHtml( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error instanceof Error ? error.stack || error.message : String(error), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )}</pre></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,6 +135,19 @@ function renderError( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const HTML_ESCAPES: Record<string, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "&": "&", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "<": "<", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ">": ">", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '"': """, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "'": "'", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // F59: escape untrusted text before interpolating into the dev error page HTML. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function escapeHtml(str: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return str.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function printInfo(cliOpts: CLIOptions, loaded: Awaited<ReturnType<typeof loadServerEntry>>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let entryInfo: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (loaded.notFound) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // Test runner: invokes the srvx CLI from source (no dist build required). | ||
| // Node strips TypeScript types on the fly (Node >= 22.18 / 24). | ||
| import { main } from "../src/cli.ts"; | ||
|
|
||
| // Point any forked child at this same source runner instead of ./bin/srvx.mjs. | ||
| (globalThis as any).__SRVX_BIN__ = new URL("./_cli-run.ts", import.meta.url).href; | ||
|
|
||
| await main({ | ||
| usage: { | ||
| command: "srvx", | ||
| docs: "https://srvx.h3.dev", | ||
| issues: "https://github.com/h3js/srvx/issues", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: h3js/srvx
Length of output: 211
🏁 Script executed:
Repository: h3js/srvx
Length of output: 3373
🏁 Script executed:
Repository: h3js/srvx
Length of output: 8162
🌐 Web query:
supports-color FORCE_COLOR 0 disable colors documentation💡 Result:
To disable color output in command-line applications that utilize the supports-color library or follow its conventions, you can set the environment variable FORCE_COLOR to 0 [1][2][3]. When FORCE_COLOR=0 is set, it explicitly overrides other terminal color support detection mechanisms and forces the application to disable ANSI color output [1][2][4][5]. In addition to this, the NO_COLOR standard is widely supported across the industry as a way to disable color output [6][7]. According to the NO_COLOR specification, any non-empty value for the NO_COLOR environment variable (e.g., NO_COLOR=1) should signal applications to refrain from adding ANSI color to their output [6][8][5]. While both are used to disable colors, FORCE_COLOR=0 is specific to libraries and tools that implement the supports-color convention, whereas NO_COLOR is a broader, cross-platform standard intended for any CLI software [9][10][6]. If both are present, the specific application's implementation determines priority, though FORCE_COLOR is often treated as having higher precedence in libraries that support both [9][10].
Citations:
Handle
FORCE_COLOR=0explicitly.env.FORCE_COLORis truthy for"0", so this branch enables colors when the convention expects them to be disabled. Parse the env value instead of checking truthiness.🤖 Prompt for AI Agents