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 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 neon --style-css ./srv-listing.css
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
- Highlights:
78
-
79
- - `-p, --port <number>`
80
- - `--host <host>`
81
- - `--open [path]`, `--no-open`
82
- - `--watch <path>` (repeat)
83
- - `--ignore <glob>` (repeat)
84
- - `--single`
85
- - `--cors`
86
- - `--no-css-inject`
87
- - `--no-dir-listing`
88
- - `--style <midnight|paper|neon>`
89
- - `--style-css <file>`
90
- - `-c` (create `srv.config.json` in served root if missing)
91
- - `--log-level <0-3>`
92
- - `--no-request-logging`
93
- - `--ssl-cert <file> --ssl-key <file> [--ssl-pass <file>]`
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.4.1",
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/elouan/srv-it",
12
+ "homepage": "https://github.com/elouangrimm/srv-it",
13
13
  "bugs": {
14
- "url": "https://github.com/elouan/srv-it/issues"
14
+ "url": "https://github.com/elouangrimm/srv-it/issues"
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/elouan/srv-it.git"
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 <name>')} Listing style preset: midnight | paper | neon`,
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.green.bold('srv is running')}`,
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(' ERROR '), ...message),
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: '#f59e0b',
49
- borderStrong: '#a8a29e',
50
- },
51
- neon: {
52
- accent: '#22c55e',
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: var(--stone-950);
136
- --bg-raised: var(--stone-900);
137
- --bg-surface: var(--stone-800);
138
- --border: var(--stone-800);
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: var(--stone-200);
141
- --text-muted: var(--stone-400);
142
- --text-faint: var(--stone-500);
143
- --text-bright: var(--stone-100);
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(--text-bright);
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(--border-strong);
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(--text-faint);
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
- await closeWatcher();
514
- wss.close();
515
- await new Promise((resolve) => server.close(resolve));
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
- process.on('SIGINT', async () => {
519
- srvLogger.info('\n[srv] shutting down...');
520
- await closeAll();
521
- process.exit(0);
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);