srv-it 0.4.1 → 0.5.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 +25 -19
- package/package.json +4 -4
- package/src/cli.js +2 -2
- package/src/server.js +110 -34
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ srv-it
|
|
|
34
34
|
srv-it 3000
|
|
35
35
|
srv-it 8080 ./public
|
|
36
36
|
srv-it ./public --open
|
|
37
|
-
srv-it --style
|
|
37
|
+
srv-it --style paper --style-css ./srv-listing.css
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Config defaults
|
|
@@ -43,7 +43,7 @@ You can set defaults globally and per-project:
|
|
|
43
43
|
|
|
44
44
|
- global: `~/.srvrc.json`
|
|
45
45
|
- project: `./srv.config.json`
|
|
46
|
-
- override file: `srv --config ./my-srv.json`
|
|
46
|
+
- override file: `srv-it --config ./my-srv.json`
|
|
47
47
|
- template: `./srv.config.example.json`
|
|
48
48
|
|
|
49
49
|
Example `srv.config.json`:
|
|
@@ -74,23 +74,29 @@ Run:
|
|
|
74
74
|
srv-it --help
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- `-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- `--
|
|
83
|
-
- `--
|
|
84
|
-
- `--
|
|
85
|
-
- `--
|
|
86
|
-
- `--
|
|
87
|
-
- `--no-
|
|
88
|
-
- `--
|
|
89
|
-
- `--
|
|
90
|
-
-
|
|
91
|
-
- `--
|
|
92
|
-
- `--
|
|
93
|
-
-
|
|
77
|
+
### Supported Arguments
|
|
78
|
+
|
|
79
|
+
- `-h, --help`: show help output
|
|
80
|
+
- `-v, --version`: print the current version
|
|
81
|
+
- `-p, --port <number>`: set the server port
|
|
82
|
+
- `--host <host>`: set the bind host (default: `0.0.0.0`)
|
|
83
|
+
- `--open [path]`: open a browser to `/` or the provided path
|
|
84
|
+
- `--no-open`: disable automatic browser opening
|
|
85
|
+
- `--watch <path>` (repeat): add extra files/folders to watch for live reload
|
|
86
|
+
- `--ignore <glob>` (repeat): ignore matching watcher paths/globs
|
|
87
|
+
- `--no-css-inject`: use full page reload for CSS changes instead of hot CSS refresh
|
|
88
|
+
- `--cors`: enable CORS headers
|
|
89
|
+
- `--single`: serve `index.html` for unknown routes (SPA fallback)
|
|
90
|
+
- `--no-dir-listing`: disable generated directory listing pages
|
|
91
|
+
- `--style <midnight|paper>`: choose the directory listing style preset
|
|
92
|
+
- `--style-css <file>`: load custom CSS for directory listing pages
|
|
93
|
+
- `-c`: create `srv.config.json` in the served root if missing
|
|
94
|
+
- `--config <file>`: read additional config JSON file
|
|
95
|
+
- `--no-request-logging`: disable request logs
|
|
96
|
+
- `--log-level <0-3>`: set startup log verbosity
|
|
97
|
+
- `--ssl-cert <file>`: path to SSL certificate
|
|
98
|
+
- `--ssl-key <file>`: path to SSL private key
|
|
99
|
+
- `--ssl-pass <file>`: path to SSL passphrase file
|
|
94
100
|
|
|
95
101
|
## Notes
|
|
96
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "srv-it",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Static server with polished CLI UI, directory listing, and live reload",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"server",
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"directory-listing",
|
|
10
10
|
"cli"
|
|
11
11
|
],
|
|
12
|
-
"homepage": "https://github.com/
|
|
12
|
+
"homepage": "https://github.com/elouangrimm/srv-it",
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/elouangrimm/srv-it/issues"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/
|
|
18
|
+
"url": "git+https://github.com/elouangrimm/srv-it.git"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"author": "Elouan Grimm",
|
package/src/cli.js
CHANGED
|
@@ -46,7 +46,7 @@ function getHelpText() {
|
|
|
46
46
|
` ${chalk.yellow('--cors')} Enable CORS`,
|
|
47
47
|
` ${chalk.yellow('--single')} SPA fallback to /index.html`,
|
|
48
48
|
` ${chalk.yellow('--no-dir-listing')} Disable directory listing`,
|
|
49
|
-
` ${chalk.yellow('--style <
|
|
49
|
+
` ${chalk.yellow('--style <midnight|paper>')} Listing style preset`,
|
|
50
50
|
` ${chalk.yellow('--style-css <file>')} Custom CSS file for listing page`,
|
|
51
51
|
` ${chalk.yellow('-c')} Create srv.config.json in served root if missing`,
|
|
52
52
|
` ${chalk.yellow('--config <file>')} Read additional config JSON file`,
|
|
@@ -207,7 +207,7 @@ async function run() {
|
|
|
207
207
|
|
|
208
208
|
if (options.logLevel >= 1) {
|
|
209
209
|
const lines = [
|
|
210
|
-
`${chalk.
|
|
210
|
+
`${chalk.hex("#3b82f6").bold('srv-it is running')}`,
|
|
211
211
|
'',
|
|
212
212
|
`${chalk.bold('- Local:')} ${url}`,
|
|
213
213
|
`${chalk.bold('- Root:')} ${options.root}`,
|
package/src/server.js
CHANGED
|
@@ -15,7 +15,7 @@ const srvLogger = {
|
|
|
15
15
|
http: (...message) => console.info(chalk.bgBlue.bold(' HTTP '), ...message),
|
|
16
16
|
info: (...message) => console.info(chalk.bgMagenta.bold(' INFO '), ...message),
|
|
17
17
|
warn: (...message) => console.error(chalk.bgYellow.bold(' WARN '), ...message),
|
|
18
|
-
error: (...message) => console.error(chalk.bgRed.bold('
|
|
18
|
+
error: (...message) => console.error(chalk.bgRed.bold(' ERRR '), ...message),
|
|
19
19
|
log: console.log,
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -42,15 +42,29 @@ const LIVE_RELOAD_SNIPPET = `\n<script>\n(function(){\n if(!('WebSocket' in win
|
|
|
42
42
|
const STYLE_PRESETS = {
|
|
43
43
|
midnight: {
|
|
44
44
|
accent: '#3b82f6',
|
|
45
|
+
accentSoft: 'rgba(59, 130, 246, 0.16)',
|
|
46
|
+
bg: '#0c0a09',
|
|
47
|
+
bgRaised: '#1c1917',
|
|
48
|
+
bgSurface: '#292524',
|
|
49
|
+
border: '#292524',
|
|
45
50
|
borderStrong: '#78716c',
|
|
51
|
+
text: '#e7e5e4',
|
|
52
|
+
textMuted: '#a8a29e',
|
|
53
|
+
textFaint: '#78716c',
|
|
54
|
+
textBright: '#f5f5f4',
|
|
46
55
|
},
|
|
47
56
|
paper: {
|
|
48
|
-
accent: '#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
accent: '#d97706',
|
|
58
|
+
accentSoft: 'rgba(217, 119, 6, 0.16)',
|
|
59
|
+
bg: '#f5f5f4',
|
|
60
|
+
bgRaised: '#fafaf9',
|
|
61
|
+
bgSurface: '#e7e5e4',
|
|
62
|
+
border: '#d6d3d1',
|
|
53
63
|
borderStrong: '#a8a29e',
|
|
64
|
+
text: '#292524',
|
|
65
|
+
textMuted: '#57534e',
|
|
66
|
+
textFaint: '#78716c',
|
|
67
|
+
textBright: '#1c1917',
|
|
54
68
|
},
|
|
55
69
|
};
|
|
56
70
|
|
|
@@ -132,16 +146,17 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
132
146
|
--stone-900: #1c1917;
|
|
133
147
|
--stone-950: #0c0a09;
|
|
134
148
|
|
|
135
|
-
--bg:
|
|
136
|
-
--bg-raised:
|
|
137
|
-
--bg-surface:
|
|
138
|
-
--border:
|
|
149
|
+
--bg: ${palette.bg};
|
|
150
|
+
--bg-raised: ${palette.bgRaised};
|
|
151
|
+
--bg-surface: ${palette.bgSurface};
|
|
152
|
+
--border: ${palette.border};
|
|
139
153
|
--border-strong: ${palette.borderStrong};
|
|
140
|
-
--text:
|
|
141
|
-
--text-muted:
|
|
142
|
-
--text-faint:
|
|
143
|
-
--text-bright:
|
|
154
|
+
--text: ${palette.text};
|
|
155
|
+
--text-muted: ${palette.textMuted};
|
|
156
|
+
--text-faint: ${palette.textFaint};
|
|
157
|
+
--text-bright: ${palette.textBright};
|
|
144
158
|
--accent: ${palette.accent};
|
|
159
|
+
--accent-soft: ${palette.accentSoft};
|
|
145
160
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
146
161
|
--font-mono: "JetBrains Mono", "SF Mono", "Fira Code", "Roboto Mono", "Cascadia Code", monospace;
|
|
147
162
|
--line-height: 1.6;
|
|
@@ -175,7 +190,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
175
190
|
|
|
176
191
|
h1 {
|
|
177
192
|
margin-bottom: 0.25rem;
|
|
178
|
-
color: var(--
|
|
193
|
+
color: var(--accent);
|
|
179
194
|
font-family: var(--font-mono);
|
|
180
195
|
font-size: 1.1rem;
|
|
181
196
|
line-height: var(--line-height-tight);
|
|
@@ -188,6 +203,8 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
188
203
|
color: var(--text-muted);
|
|
189
204
|
font-family: var(--font-mono);
|
|
190
205
|
font-size: 0.9rem;
|
|
206
|
+
border-left: 2px solid var(--accent);
|
|
207
|
+
padding-left: 0.6rem;
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
.up {
|
|
@@ -195,10 +212,10 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
195
212
|
margin-bottom: 0.75rem;
|
|
196
213
|
color: var(--accent);
|
|
197
214
|
text-decoration: none;
|
|
198
|
-
border: 1px solid var(--
|
|
215
|
+
border: 1px solid var(--accent);
|
|
199
216
|
padding: 0.3rem 0.55rem;
|
|
200
217
|
font-family: var(--font-mono);
|
|
201
|
-
transition: background-color 0.18s ease, color 0.18s ease;
|
|
218
|
+
transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
.up:hover {
|
|
@@ -221,7 +238,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
221
238
|
color: var(--text);
|
|
222
239
|
border-bottom: 1px solid var(--border);
|
|
223
240
|
font-family: var(--font-mono);
|
|
224
|
-
transition: background-color 0.18s ease, color 0.18s ease;
|
|
241
|
+
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
|
|
225
242
|
}
|
|
226
243
|
|
|
227
244
|
li:last-child a {
|
|
@@ -231,6 +248,7 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
231
248
|
li a:hover {
|
|
232
249
|
background-color: var(--bg-surface);
|
|
233
250
|
color: var(--text-bright);
|
|
251
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
.name {
|
|
@@ -241,10 +259,13 @@ function renderDirListing({ pathnameValue, entries, style, customCss }) {
|
|
|
241
259
|
}
|
|
242
260
|
|
|
243
261
|
.type {
|
|
244
|
-
color: var(--
|
|
262
|
+
color: var(--accent);
|
|
245
263
|
text-transform: uppercase;
|
|
246
264
|
font-size: 0.72rem;
|
|
247
265
|
letter-spacing: 0.07em;
|
|
266
|
+
padding: 0.12rem 0.35rem;
|
|
267
|
+
border: 1px solid var(--accent-soft);
|
|
268
|
+
background-color: var(--accent-soft);
|
|
248
269
|
}
|
|
249
270
|
|
|
250
271
|
@media (max-width: 768px) {
|
|
@@ -290,6 +311,7 @@ async function createSrvServer(options) {
|
|
|
290
311
|
const root = path.resolve(options.root);
|
|
291
312
|
const compress = getCompressionMiddleware();
|
|
292
313
|
const clients = new Set();
|
|
314
|
+
const sockets = new Set();
|
|
293
315
|
|
|
294
316
|
let customCss = '';
|
|
295
317
|
if (options.styleCss) {
|
|
@@ -327,6 +349,12 @@ async function createSrvServer(options) {
|
|
|
327
349
|
try {
|
|
328
350
|
stat = await fsp.stat(filePath);
|
|
329
351
|
} catch (error) {
|
|
352
|
+
if (pathnameValue === '/sw.js' && error && error.code === 'ENOENT') {
|
|
353
|
+
// Browsers often probe /sw.js by default; treat missing file as a silent no-op.
|
|
354
|
+
res.statusCode = 204;
|
|
355
|
+
res.end();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
330
358
|
if (options.single) {
|
|
331
359
|
filePath = path.join(root, 'index.html');
|
|
332
360
|
stat = await fsp.stat(filePath);
|
|
@@ -397,11 +425,15 @@ async function createSrvServer(options) {
|
|
|
397
425
|
res.statusCode = 500;
|
|
398
426
|
res.end('Internal server error');
|
|
399
427
|
if (options.logLevel >= 1) {
|
|
400
|
-
srvLogger.error('[srv] request error:', error.message);
|
|
428
|
+
srvLogger.error('[srv-it] request error:', error.message);
|
|
401
429
|
}
|
|
402
430
|
} finally {
|
|
403
431
|
if (!options.noRequestLogging) {
|
|
404
432
|
const statusCode = res.statusCode;
|
|
433
|
+
const suppressRequestLog = pathnameValue === '/sw.js' && statusCode === 204;
|
|
434
|
+
if (suppressRequestLog) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
405
437
|
const elapsed = Date.now() - start;
|
|
406
438
|
const sourceIp = (req.socket.remoteAddress || '-').replace('::ffff:', '');
|
|
407
439
|
const now = new Date();
|
|
@@ -432,7 +464,7 @@ async function createSrvServer(options) {
|
|
|
432
464
|
res.statusCode = 500;
|
|
433
465
|
res.end('Internal server error');
|
|
434
466
|
if (options.logLevel >= 1) {
|
|
435
|
-
srvLogger.error('[srv] handler error:', error.message);
|
|
467
|
+
srvLogger.error('[srv-it] handler error:', error.message);
|
|
436
468
|
}
|
|
437
469
|
});
|
|
438
470
|
});
|
|
@@ -442,12 +474,17 @@ async function createSrvServer(options) {
|
|
|
442
474
|
res.statusCode = 500;
|
|
443
475
|
res.end('Internal server error');
|
|
444
476
|
if (options.logLevel >= 1) {
|
|
445
|
-
srvLogger.error('[srv] handler error:', error.message);
|
|
477
|
+
srvLogger.error('[srv-it] handler error:', error.message);
|
|
446
478
|
}
|
|
447
479
|
});
|
|
448
480
|
});
|
|
449
481
|
}
|
|
450
482
|
|
|
483
|
+
server.on('connection', (socket) => {
|
|
484
|
+
sockets.add(socket);
|
|
485
|
+
socket.on('close', () => sockets.delete(socket));
|
|
486
|
+
});
|
|
487
|
+
|
|
451
488
|
const wss = new WebSocketServer({ server, path: '/__srv_ws' });
|
|
452
489
|
wss.on('connection', (socket) => {
|
|
453
490
|
clients.add(socket);
|
|
@@ -486,7 +523,7 @@ async function createSrvServer(options) {
|
|
|
486
523
|
}
|
|
487
524
|
}
|
|
488
525
|
if (options.logLevel >= 2) {
|
|
489
|
-
srvLogger.info(`[srv] ${isCss ? 'css refresh' : 'reload'}: ${changePath}`);
|
|
526
|
+
srvLogger.info(`[srv-it] ${isCss ? 'css refresh' : 'reload'}: ${changePath}`);
|
|
490
527
|
}
|
|
491
528
|
};
|
|
492
529
|
|
|
@@ -499,27 +536,66 @@ async function createSrvServer(options) {
|
|
|
499
536
|
if (error && error.code === 'ENOSPC') {
|
|
500
537
|
liveReloadEnabled = false;
|
|
501
538
|
await closeWatcher();
|
|
502
|
-
srvLogger.error('[srv] live reload disabled: file watcher limit reached (ENOSPC).');
|
|
503
|
-
srvLogger.warn('[srv] use --ignore to exclude noisy paths, or raise inotify limits on Linux.');
|
|
539
|
+
srvLogger.error('[srv-it] live reload disabled: file watcher limit reached (ENOSPC).');
|
|
540
|
+
srvLogger.warn('[srv-it] use --ignore to exclude noisy paths, or raise inotify limits on Linux.');
|
|
504
541
|
return;
|
|
505
542
|
}
|
|
506
543
|
|
|
507
544
|
if (options.logLevel >= 1) {
|
|
508
|
-
srvLogger.error(`[srv] watcher error: ${error.message}`);
|
|
545
|
+
srvLogger.error(`[srv-it] watcher error: ${error.message}`);
|
|
509
546
|
}
|
|
510
547
|
});
|
|
511
548
|
|
|
549
|
+
let closingPromise;
|
|
512
550
|
const closeAll = async () => {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
551
|
+
if (closingPromise) {
|
|
552
|
+
return closingPromise;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
closingPromise = (async () => {
|
|
556
|
+
await closeWatcher();
|
|
557
|
+
|
|
558
|
+
for (const client of clients) {
|
|
559
|
+
try {
|
|
560
|
+
client.terminate();
|
|
561
|
+
} catch (_error) {
|
|
562
|
+
// Ignore client termination errors during shutdown.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await new Promise((resolve) => wss.close(resolve));
|
|
567
|
+
|
|
568
|
+
for (const socket of sockets) {
|
|
569
|
+
socket.destroy();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
await new Promise((resolve) => server.close(resolve));
|
|
573
|
+
})();
|
|
574
|
+
|
|
575
|
+
return closingPromise;
|
|
516
576
|
};
|
|
517
577
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
578
|
+
let shuttingDown = false;
|
|
579
|
+
const onSigint = async () => {
|
|
580
|
+
if (shuttingDown) {
|
|
581
|
+
process.exit(130);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
shuttingDown = true;
|
|
586
|
+
process.stdout.write('\u001B[2K\r');
|
|
587
|
+
console.log(chalk.bgWhite.bold('\n[srv-it]') + ' shutting down...');
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
await closeAll();
|
|
591
|
+
process.exit(0);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
srvLogger.error('[srv-it] shutdown error:', error.message);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
process.once('SIGINT', onSigint);
|
|
523
599
|
|
|
524
600
|
await new Promise((resolve, reject) => {
|
|
525
601
|
server.once('error', reject);
|