@dnax/serve 0.0.0-stage → 0.0.1
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 +81 -2
- package/index.ts +8 -0
- package/package.json +25 -4
- package/src/cli.ts +594 -0
- package/src/listing.ts +173 -0
- package/src/mime.ts +154 -0
- package/src/server.ts +861 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { statSync } from "node:fs";
|
|
4
|
+
import { networkInterfaces } from "node:os";
|
|
5
|
+
import { startStaticServer, type ServeOptions } from "./server.ts";
|
|
6
|
+
|
|
7
|
+
type Value = string | boolean | string[];
|
|
8
|
+
|
|
9
|
+
interface ParsedArgs {
|
|
10
|
+
values: Map<string, Value>;
|
|
11
|
+
positionals: string[];
|
|
12
|
+
unknown: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SHORT: Readonly<Record<string, string>> = {
|
|
16
|
+
p: "port",
|
|
17
|
+
a: "address",
|
|
18
|
+
d: "showDir",
|
|
19
|
+
i: "autoIndex",
|
|
20
|
+
g: "gzip",
|
|
21
|
+
b: "brotli",
|
|
22
|
+
e: "ext",
|
|
23
|
+
s: "silent",
|
|
24
|
+
o: "open",
|
|
25
|
+
c: "cache",
|
|
26
|
+
t: "timeout",
|
|
27
|
+
T: "title",
|
|
28
|
+
U: "utc",
|
|
29
|
+
P: "proxy",
|
|
30
|
+
S: "tls",
|
|
31
|
+
C: "cert",
|
|
32
|
+
K: "key",
|
|
33
|
+
r: "robots",
|
|
34
|
+
H: "header",
|
|
35
|
+
v: "version",
|
|
36
|
+
h: "help",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ALIASES: Readonly<Record<string, string>> = {
|
|
40
|
+
dir: "showDir",
|
|
41
|
+
"auto-index": "autoIndex",
|
|
42
|
+
"base-dir": "baseDir",
|
|
43
|
+
"dir-overrides-404": "dirOverrides404",
|
|
44
|
+
"private-network-access": "privateNetworkAccess",
|
|
45
|
+
"log-ip": "logIp",
|
|
46
|
+
"hide-permissions": "hidePermissions",
|
|
47
|
+
"proxy-all": "proxyAll",
|
|
48
|
+
ssl: "tls",
|
|
49
|
+
user: "username",
|
|
50
|
+
"allowed-hosts": "allowedHosts",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const BOOLEAN_KEYS = new Set([
|
|
54
|
+
"showDir",
|
|
55
|
+
"autoIndex",
|
|
56
|
+
"gzip",
|
|
57
|
+
"brotli",
|
|
58
|
+
"silent",
|
|
59
|
+
"coop",
|
|
60
|
+
"privateNetworkAccess",
|
|
61
|
+
"utc",
|
|
62
|
+
"logIp",
|
|
63
|
+
"robots",
|
|
64
|
+
"dotfiles",
|
|
65
|
+
"dirOverrides404",
|
|
66
|
+
"hidePermissions",
|
|
67
|
+
"proxyAll",
|
|
68
|
+
"tls",
|
|
69
|
+
"help",
|
|
70
|
+
"version",
|
|
71
|
+
"panic",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const OPTIONAL_VALUE_KEYS = new Set(["cors", "open"]);
|
|
75
|
+
|
|
76
|
+
const VALUE_KEYS = new Set([
|
|
77
|
+
"port",
|
|
78
|
+
"address",
|
|
79
|
+
"baseDir",
|
|
80
|
+
"ext",
|
|
81
|
+
"cache",
|
|
82
|
+
"timeout",
|
|
83
|
+
"title",
|
|
84
|
+
"proxy",
|
|
85
|
+
"username",
|
|
86
|
+
"password",
|
|
87
|
+
"cert",
|
|
88
|
+
"key",
|
|
89
|
+
"header",
|
|
90
|
+
"mimetypes",
|
|
91
|
+
"allowedHosts",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
function normalizeKey(key: string): string {
|
|
95
|
+
return ALIASES[key] ?? key;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseBoolean(value: string): boolean {
|
|
99
|
+
return value !== "false";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseArgv(argv: string[]): ParsedArgs {
|
|
103
|
+
const values = new Map<string, Value>();
|
|
104
|
+
const positionals: string[] = [];
|
|
105
|
+
const unknown: string[] = [];
|
|
106
|
+
|
|
107
|
+
const setValue = (key: string, value: Value): void => {
|
|
108
|
+
if (key === "header" && typeof value === "string") {
|
|
109
|
+
const existing = values.get("header");
|
|
110
|
+
if (Array.isArray(existing)) existing.push(value);
|
|
111
|
+
else if (typeof existing === "string") values.set("header", [existing, value]);
|
|
112
|
+
else values.set("header", [value]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
values.set(key, value);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
119
|
+
const arg = argv[index];
|
|
120
|
+
if (arg === undefined) continue;
|
|
121
|
+
|
|
122
|
+
if (arg === "--") {
|
|
123
|
+
positionals.push(...argv.slice(index + 1));
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (arg.startsWith("--")) {
|
|
128
|
+
let name = arg.slice(2);
|
|
129
|
+
let inline: string | undefined;
|
|
130
|
+
const equals = name.indexOf("=");
|
|
131
|
+
if (equals !== -1) {
|
|
132
|
+
inline = name.slice(equals + 1);
|
|
133
|
+
name = name.slice(0, equals);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let negated = false;
|
|
137
|
+
if (name.startsWith("no-")) {
|
|
138
|
+
negated = true;
|
|
139
|
+
name = name.slice(3);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const key = normalizeKey(name);
|
|
143
|
+
|
|
144
|
+
if (negated) {
|
|
145
|
+
values.set(key, false);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
150
|
+
const next = argv[index + 1];
|
|
151
|
+
if (inline !== undefined) values.set(key, parseBoolean(inline));
|
|
152
|
+
else if (next === "true" || next === "false") {
|
|
153
|
+
values.set(key, parseBoolean(next));
|
|
154
|
+
index += 1;
|
|
155
|
+
} else values.set(key, true);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (OPTIONAL_VALUE_KEYS.has(key)) {
|
|
160
|
+
values.set(key, inline ?? true);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (VALUE_KEYS.has(key)) {
|
|
165
|
+
if (inline !== undefined) {
|
|
166
|
+
setValue(key, inline);
|
|
167
|
+
} else {
|
|
168
|
+
const next = argv[index + 1];
|
|
169
|
+
if (next !== undefined) {
|
|
170
|
+
setValue(key, next);
|
|
171
|
+
index += 1;
|
|
172
|
+
} else {
|
|
173
|
+
setValue(key, true);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
unknown.push(arg);
|
|
180
|
+
const next = argv[index + 1];
|
|
181
|
+
if (inline === undefined && next !== undefined && !next.startsWith("-")) index += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
186
|
+
let offset = 1;
|
|
187
|
+
while (offset < arg.length) {
|
|
188
|
+
const character = arg[offset] ?? "";
|
|
189
|
+
const key = normalizeKey(SHORT[character] ?? character);
|
|
190
|
+
const known = BOOLEAN_KEYS.has(key) || OPTIONAL_VALUE_KEYS.has(key) || VALUE_KEYS.has(key);
|
|
191
|
+
|
|
192
|
+
if (!known) {
|
|
193
|
+
unknown.push(`-${character}`);
|
|
194
|
+
offset += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
199
|
+
const next = argv[index + 1];
|
|
200
|
+
if (next === "true" || next === "false") {
|
|
201
|
+
values.set(key, parseBoolean(next));
|
|
202
|
+
index += 1;
|
|
203
|
+
} else {
|
|
204
|
+
values.set(key, true);
|
|
205
|
+
}
|
|
206
|
+
offset += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (OPTIONAL_VALUE_KEYS.has(key)) {
|
|
211
|
+
const rest = arg.slice(offset + 1).replace(/^=/, "");
|
|
212
|
+
if (rest) {
|
|
213
|
+
setValue(key, rest);
|
|
214
|
+
} else {
|
|
215
|
+
const next = argv[index + 1];
|
|
216
|
+
if (
|
|
217
|
+
key === "open" &&
|
|
218
|
+
next !== undefined &&
|
|
219
|
+
!next.startsWith("-") &&
|
|
220
|
+
(next.startsWith("/") || /^https?:\/\//i.test(next))
|
|
221
|
+
) {
|
|
222
|
+
setValue(key, next);
|
|
223
|
+
index += 1;
|
|
224
|
+
} else {
|
|
225
|
+
values.set(key, true);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
offset = arg.length;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let rest = arg.slice(offset + 1);
|
|
233
|
+
if (rest.startsWith("=")) rest = rest.slice(1);
|
|
234
|
+
if (rest) {
|
|
235
|
+
setValue(key, rest);
|
|
236
|
+
} else {
|
|
237
|
+
const next = argv[index + 1];
|
|
238
|
+
if (next !== undefined) {
|
|
239
|
+
setValue(key, next);
|
|
240
|
+
index += 1;
|
|
241
|
+
} else {
|
|
242
|
+
setValue(key, true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
offset = arg.length;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
positionals.push(arg);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { values, positionals, unknown };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function boolValue(values: Map<string, Value>, key: string, fallback: boolean): boolean {
|
|
257
|
+
const value = values.get(key);
|
|
258
|
+
if (value === undefined) return fallback;
|
|
259
|
+
if (typeof value === "boolean") return value;
|
|
260
|
+
if (typeof value === "string") return value !== "false";
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function stringValue(values: Map<string, Value>, key: string): string | undefined {
|
|
265
|
+
const value = values.get(key);
|
|
266
|
+
if (value === undefined) return undefined;
|
|
267
|
+
if (typeof value === "string") return value;
|
|
268
|
+
if (Array.isArray(value)) return value.at(-1);
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function numberValue(
|
|
273
|
+
values: Map<string, Value>,
|
|
274
|
+
key: string,
|
|
275
|
+
label: string,
|
|
276
|
+
): number | undefined {
|
|
277
|
+
const value = values.get(key);
|
|
278
|
+
if (value === undefined || value === true) return undefined;
|
|
279
|
+
const raw = Array.isArray(value) ? value.at(-1) : value;
|
|
280
|
+
const parsed = Number(raw);
|
|
281
|
+
if (!Number.isFinite(parsed)) throw new Error(`Invalid ${label}: ${String(raw)}`);
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function headersValue(values: Map<string, Value>): Record<string, string> {
|
|
286
|
+
const value = values.get("header");
|
|
287
|
+
const entries = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
288
|
+
const headers: Record<string, string> = {};
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
const separator = entry.indexOf(":");
|
|
291
|
+
if (separator === -1) {
|
|
292
|
+
process.stderr.write(`Warning: ignoring malformed header "${entry}" (expected "Name: value")\n`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
headers[entry.slice(0, separator).trim()] = entry.slice(separator + 1).trim();
|
|
296
|
+
}
|
|
297
|
+
return headers;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function directoryExists(path: string): boolean {
|
|
301
|
+
try {
|
|
302
|
+
return statSync(path).isDirectory();
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function displayRoot(root: string): string {
|
|
309
|
+
return root;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function formatTimeout(timeout: number): string {
|
|
313
|
+
return timeout === 0 ? "disabled" : `${timeout} seconds`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatCache(cache: number | undefined): string {
|
|
317
|
+
if (cache === undefined) return "3600 seconds";
|
|
318
|
+
return cache === -1 ? "disabled" : `${cache} seconds`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function colorize(): (code: number, text: string) => string {
|
|
322
|
+
const enabled = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
323
|
+
return (code, text) => (enabled ? `\u001b[${code}m${text}\u001b[0m` : text);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function availableUrls(address: string, port: number, protocol: string): string[] {
|
|
327
|
+
if (address !== "0.0.0.0" && address !== "::" && address !== "") {
|
|
328
|
+
return [`${protocol}://${address}:${port}`];
|
|
329
|
+
}
|
|
330
|
+
const urls = new Set<string>([`${protocol}://127.0.0.1:${port}`]);
|
|
331
|
+
const interfaces = networkInterfaces();
|
|
332
|
+
for (const entries of Object.values(interfaces)) {
|
|
333
|
+
for (const entry of entries ?? []) {
|
|
334
|
+
if (entry.family === "IPv4" && !entry.internal) {
|
|
335
|
+
urls.add(`${protocol}://${entry.address}:${port}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return [...urls];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function openBrowser(url: string): void {
|
|
343
|
+
const command =
|
|
344
|
+
process.platform === "darwin"
|
|
345
|
+
? ["open", url]
|
|
346
|
+
: process.platform === "win32"
|
|
347
|
+
? ["cmd", "/c", "start", "", url]
|
|
348
|
+
: ["xdg-open", url];
|
|
349
|
+
try {
|
|
350
|
+
Bun.spawn(command, { stdio: ["ignore", "ignore", "ignore"] });
|
|
351
|
+
} catch {
|
|
352
|
+
// Opening the browser is best-effort.
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const HELP = `usage: dnx serve [path] [options]
|
|
357
|
+
|
|
358
|
+
A Bun static file server compatible with the http-server CLI.
|
|
359
|
+
|
|
360
|
+
The "serve" subcommand is optional: "dnx [path] [options]" also works.
|
|
361
|
+
|
|
362
|
+
options:
|
|
363
|
+
-p --port Port to use. If 0, look for an open port starting at 8080. [8080]
|
|
364
|
+
-a --address Address to use [0.0.0.0]
|
|
365
|
+
--base-dir Base path to serve files from [/]
|
|
366
|
+
-d --dir Show directory listings [true]
|
|
367
|
+
-i --autoIndex Display index.html for directories [true]
|
|
368
|
+
--dir-overrides-404 Let -d override the magic 404.html file [false]
|
|
369
|
+
-g --gzip Serve precompressed .gz files when accepted [false]
|
|
370
|
+
-b --brotli Serve precompressed .br files when accepted [false]
|
|
371
|
+
If both are enabled, brotli takes precedence.
|
|
372
|
+
-e --ext Default file extension if none supplied [html]
|
|
373
|
+
-s --silent Suppress log messages from output
|
|
374
|
+
--cors[=headers] Enable CORS via the Access-Control-Allow-Origin header.
|
|
375
|
+
Optionally provide extra allowed headers, comma separated.
|
|
376
|
+
--coop Enable Cross-Origin-Opener-Policy: same-origin
|
|
377
|
+
--private-network-access Enable Private Network Access
|
|
378
|
+
-H --header Add an extra response header (may be repeated), e.g. -H "X-Foo: bar"
|
|
379
|
+
-o [path] Open the browser after starting the server.
|
|
380
|
+
-c --cache Cache time (max-age) in seconds [3600], e.g. -c10 or -c-1 to disable
|
|
381
|
+
-t --timeout Connection idle timeout in seconds [120], 0 to disable
|
|
382
|
+
-T --title Extra text for the terminal title
|
|
383
|
+
-U --utc Use UTC time format in log messages
|
|
384
|
+
--log-ip Include the client IP in log messages
|
|
385
|
+
-P --proxy Fallback proxy for requests that cannot be resolved, e.g. http://localhost:3000
|
|
386
|
+
--proxy-all Forward every request to the proxy
|
|
387
|
+
--username Username for basic authentication (env: NODE_HTTP_SERVER_USERNAME)
|
|
388
|
+
--password Password for basic authentication (env: NODE_HTTP_SERVER_PASSWORD)
|
|
389
|
+
-S --tls --ssl Enable secure request serving with TLS/SSL (HTTPS)
|
|
390
|
+
-C --cert Path to the TLS certificate file [cert.pem]
|
|
391
|
+
-K --key Path to the TLS key file [key.pem]
|
|
392
|
+
-r --robots Respond to /robots.txt with "User-agent: *\\nDisallow: /"
|
|
393
|
+
--no-dotfiles Do not serve dotfiles
|
|
394
|
+
--hide-permissions Hide permissions in directory listings
|
|
395
|
+
--mimetypes Path to a .types file (or inline JSON) for custom MIME types
|
|
396
|
+
--allowed-hosts Comma separated hosts allowed to access the server
|
|
397
|
+
-h --help Print this list and exit
|
|
398
|
+
-v --version Print the version and exit
|
|
399
|
+
|
|
400
|
+
Magic files: index.<ext> is served for directories and 404.<ext> for not found.
|
|
401
|
+
`;
|
|
402
|
+
|
|
403
|
+
async function readVersion(): Promise<string> {
|
|
404
|
+
try {
|
|
405
|
+
const pkg = (await Bun.file(new URL("../package.json", import.meta.url)).json()) as { version?: string };
|
|
406
|
+
return pkg.version ?? "0.0.0";
|
|
407
|
+
} catch {
|
|
408
|
+
return "0.0.0";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function main(): Promise<void> {
|
|
413
|
+
const rawArgs = process.argv.slice(2);
|
|
414
|
+
const argv = rawArgs[0] === "serve" ? rawArgs.slice(1) : rawArgs;
|
|
415
|
+
const parsed = parseArgv(argv);
|
|
416
|
+
const paint = colorize();
|
|
417
|
+
const yellow = (text: string) => paint(33, text);
|
|
418
|
+
const cyan = (text: string) => paint(36, text);
|
|
419
|
+
const green = (text: string) => paint(32, text);
|
|
420
|
+
const red = (text: string) => paint(31, text);
|
|
421
|
+
|
|
422
|
+
for (const flag of parsed.unknown) {
|
|
423
|
+
process.stderr.write(`Warning: ignoring unknown option ${flag}\n`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (boolValue(parsed.values, "help", false)) {
|
|
427
|
+
process.stdout.write(HELP);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const version = await readVersion();
|
|
432
|
+
if (boolValue(parsed.values, "version", false)) {
|
|
433
|
+
process.stdout.write(`v${version}\n`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const portValue = parsed.values.get("port");
|
|
438
|
+
let port: number;
|
|
439
|
+
if (portValue === undefined) {
|
|
440
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : NaN;
|
|
441
|
+
port = Number.isInteger(envPort) ? envPort : 8080;
|
|
442
|
+
} else if (portValue === true) {
|
|
443
|
+
port = 8080;
|
|
444
|
+
} else {
|
|
445
|
+
port = Number(Array.isArray(portValue) ? portValue.at(-1) : portValue);
|
|
446
|
+
}
|
|
447
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
448
|
+
throw new Error(`Invalid port: ${String(portValue)}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const address = stringValue(parsed.values, "address") ?? "0.0.0.0";
|
|
452
|
+
const cache = numberValue(parsed.values, "cache", "cache time");
|
|
453
|
+
const timeout = numberValue(parsed.values, "timeout", "connection timeout");
|
|
454
|
+
const extValue = parsed.values.get("ext");
|
|
455
|
+
const ext = typeof extValue === "string" ? extValue : undefined;
|
|
456
|
+
|
|
457
|
+
const rootArgument = parsed.positionals[0];
|
|
458
|
+
const root = rootArgument ?? (directoryExists("public") ? "./public" : "./");
|
|
459
|
+
|
|
460
|
+
const tls = boolValue(parsed.values, "tls", false);
|
|
461
|
+
const username = stringValue(parsed.values, "username") ?? process.env.NODE_HTTP_SERVER_USERNAME;
|
|
462
|
+
const password = stringValue(parsed.values, "password") ?? process.env.NODE_HTTP_SERVER_PASSWORD;
|
|
463
|
+
const proxy = stringValue(parsed.values, "proxy");
|
|
464
|
+
const robotsValue = parsed.values.get("robots");
|
|
465
|
+
const robots: boolean | string =
|
|
466
|
+
robotsValue === true
|
|
467
|
+
? true
|
|
468
|
+
: typeof robotsValue === "string"
|
|
469
|
+
? robotsValue === "true"
|
|
470
|
+
? true
|
|
471
|
+
: robotsValue === "false"
|
|
472
|
+
? false
|
|
473
|
+
: robotsValue
|
|
474
|
+
: false;
|
|
475
|
+
|
|
476
|
+
const corsValue = parsed.values.get("cors");
|
|
477
|
+
const cors: boolean | string = typeof corsValue === "string" ? corsValue : corsValue === true;
|
|
478
|
+
|
|
479
|
+
const allowedHosts = (stringValue(parsed.values, "allowedHosts") ?? "")
|
|
480
|
+
.split(",")
|
|
481
|
+
.map((host) => host.trim())
|
|
482
|
+
.filter(Boolean);
|
|
483
|
+
|
|
484
|
+
const options: ServeOptions = {
|
|
485
|
+
root,
|
|
486
|
+
port,
|
|
487
|
+
address,
|
|
488
|
+
baseDir: stringValue(parsed.values, "baseDir"),
|
|
489
|
+
showDir: boolValue(parsed.values, "showDir", true),
|
|
490
|
+
autoIndex: boolValue(parsed.values, "autoIndex", true),
|
|
491
|
+
dirOverrides404: boolValue(parsed.values, "dirOverrides404", false),
|
|
492
|
+
gzip: boolValue(parsed.values, "gzip", false),
|
|
493
|
+
brotli: boolValue(parsed.values, "brotli", false),
|
|
494
|
+
ext,
|
|
495
|
+
silent: boolValue(parsed.values, "silent", false),
|
|
496
|
+
cors,
|
|
497
|
+
coop: boolValue(parsed.values, "coop", false),
|
|
498
|
+
privateNetworkAccess: boolValue(parsed.values, "privateNetworkAccess", false),
|
|
499
|
+
headers: headersValue(parsed.values),
|
|
500
|
+
cache,
|
|
501
|
+
timeout,
|
|
502
|
+
utc: boolValue(parsed.values, "utc", false),
|
|
503
|
+
logIp: boolValue(parsed.values, "logIp", false),
|
|
504
|
+
proxy,
|
|
505
|
+
proxyAll: boolValue(parsed.values, "proxyAll", false),
|
|
506
|
+
username,
|
|
507
|
+
password,
|
|
508
|
+
ssl: tls,
|
|
509
|
+
cert: stringValue(parsed.values, "cert"),
|
|
510
|
+
key: stringValue(parsed.values, "key"),
|
|
511
|
+
robots,
|
|
512
|
+
dotfiles: boolValue(parsed.values, "dotfiles", true),
|
|
513
|
+
mimetypes: stringValue(parsed.values, "mimetypes"),
|
|
514
|
+
hidePermissions: boolValue(parsed.values, "hidePermissions", false),
|
|
515
|
+
allowedHosts,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const title = stringValue(parsed.values, "title");
|
|
519
|
+
const openValue = parsed.values.get("open");
|
|
520
|
+
|
|
521
|
+
let started: Awaited<ReturnType<typeof startStaticServer>>;
|
|
522
|
+
try {
|
|
523
|
+
started = await startStaticServer(options);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
process.stderr.write(`${red("Error:")} ${(error as Error).message}\n`);
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const resolved = started.options;
|
|
531
|
+
const protocol = resolved.ssl ? "https" : "http";
|
|
532
|
+
const silent = resolved.silent;
|
|
533
|
+
|
|
534
|
+
process.title = `dnx ${started.port}${title ? ` ${title}` : ""}`;
|
|
535
|
+
if (title && process.stdout.isTTY) {
|
|
536
|
+
process.stdout.write(`\u001b]0;dnx ${started.port} ${title}\u0007`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!silent) {
|
|
540
|
+
const lines: string[] = [];
|
|
541
|
+
lines.push(
|
|
542
|
+
`${yellow("Starting up dnx, serving")} ${cyan(displayRoot(root))}${
|
|
543
|
+
resolved.ssl ? ` ${yellow("through")} ${cyan("https")}` : ""
|
|
544
|
+
}`,
|
|
545
|
+
);
|
|
546
|
+
lines.push("");
|
|
547
|
+
lines.push(`${yellow("dnx version:")} ${cyan(version)}`);
|
|
548
|
+
lines.push("");
|
|
549
|
+
lines.push(yellow("dnx settings:"));
|
|
550
|
+
lines.push(`CORS: ${resolved.cors ? cyan(resolved.corsHeaders ?? "enabled") : red("disabled")}`);
|
|
551
|
+
lines.push(`Cache: ${resolved.cache === -1 ? red("disabled") : cyan(formatCache(resolved.cache))}`);
|
|
552
|
+
lines.push(`Connection Timeout: ${resolved.timeout === 0 ? red("disabled") : cyan(formatTimeout(resolved.timeout))}`);
|
|
553
|
+
lines.push(`Directory Listings: ${resolved.showDir ? cyan("visible") : red("not visible")}`);
|
|
554
|
+
lines.push(`AutoIndex: ${resolved.autoIndex ? cyan("visible") : red("not visible")}`);
|
|
555
|
+
lines.push(`Serve GZIP Files: ${resolved.gzip ? cyan("true") : red("false")}`);
|
|
556
|
+
lines.push(`Serve Brotli Files: ${resolved.brotli ? cyan("true") : red("false")}`);
|
|
557
|
+
lines.push(`Default File Extension: ${cyan(resolved.ext)}`);
|
|
558
|
+
if (resolved.ssl) lines.push(`TLS: ${cyan("enabled")} (${resolved.cert})`);
|
|
559
|
+
if (resolved.username !== undefined || resolved.password !== undefined) {
|
|
560
|
+
lines.push(`Basic Authentication: ${cyan("enabled")}`);
|
|
561
|
+
}
|
|
562
|
+
if (resolved.proxy) {
|
|
563
|
+
lines.push(`Proxy fallback: ${cyan(resolved.proxy)}${resolved.proxyAll ? " (all requests)" : ""}`);
|
|
564
|
+
}
|
|
565
|
+
lines.push("");
|
|
566
|
+
lines.push(yellow("Available on:"));
|
|
567
|
+
for (const url of availableUrls(resolved.address, started.port, protocol)) {
|
|
568
|
+
lines.push(` ${green(url)}`);
|
|
569
|
+
}
|
|
570
|
+
lines.push("Hit CTRL-C to stop the server");
|
|
571
|
+
process.stdout.write(`${lines.join("\n")}\n\n`);
|
|
572
|
+
|
|
573
|
+
if (openValue !== undefined && openValue !== false) {
|
|
574
|
+
const openHost = resolved.address === "0.0.0.0" || resolved.address === "::" ? "127.0.0.1" : resolved.address;
|
|
575
|
+
let openUrl = `${protocol}://${openHost}:${started.port}`;
|
|
576
|
+
if (typeof openValue === "string") {
|
|
577
|
+
openUrl += openValue.startsWith("/") ? openValue : `/${openValue}`;
|
|
578
|
+
}
|
|
579
|
+
process.stdout.write(`Open: ${openUrl}\n`);
|
|
580
|
+
openBrowser(openUrl);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const shutdown = (): void => {
|
|
585
|
+
if (!silent) process.stdout.write(`${red("dnx stopped.")}\n`);
|
|
586
|
+
started.stop();
|
|
587
|
+
process.exit(0);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
process.on("SIGINT", shutdown);
|
|
591
|
+
process.on("SIGTERM", shutdown);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
await main();
|