@webjsdev/server 0.8.71 → 0.8.72
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/package.json +1 -1
- package/src/dev/server.js +26 -19
- package/src/dev/watch-tree.js +104 -0
- package/src/listener-bun.js +1 -1
- package/src/listener-core.js +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.72",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Provides the file-based router, SSR, server actions, route handlers, middleware, and live reload on Node 24+ or Bun.",
|
|
6
6
|
"main": "index.js",
|
package/src/dev/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { strongerVerdict } from '../dev-classify.js';
|
|
3
|
-
import {
|
|
3
|
+
import { watchTree } from './watch-tree.js';
|
|
4
4
|
import { relative } from 'node:path';
|
|
5
5
|
import { createServer as createHttp1Server } from 'node:http';
|
|
6
6
|
import { createRequestHandler } from './handler.js';
|
|
@@ -190,7 +190,7 @@ function startNodeListener(ctx) {
|
|
|
190
190
|
// Catch-all process handlers: log, but don't tear the process down on a
|
|
191
191
|
// single mishandled promise. Uncaught exceptions are different: state may be
|
|
192
192
|
// corrupted, so log + start an orderly shutdown rather than continuing.
|
|
193
|
-
installProcessHandlers(logger, () => shutdown('uncaughtException', { fatal: true }));
|
|
193
|
+
installProcessHandlers(logger, () => shutdown('uncaughtException', { fatal: true }), { dev });
|
|
194
194
|
|
|
195
195
|
return {
|
|
196
196
|
server,
|
|
@@ -252,11 +252,12 @@ export async function startServer(opts) {
|
|
|
252
252
|
/** @type {AbortController | null} */
|
|
253
253
|
let watcherAbort = null;
|
|
254
254
|
if (dev) {
|
|
255
|
-
// Watch the app
|
|
256
|
-
// `
|
|
257
|
-
//
|
|
255
|
+
// Watch the app tree with Node's built-in `fs.watch` (no external dep):
|
|
256
|
+
// `watchTree` (#1521) watches the root plus each non-ignored top-level dir,
|
|
257
|
+
// so `node_modules` is never walked, and handles a per-path watch error
|
|
258
|
+
// (EACCES, ENOENT) with a warning instead of an uncaughtException.
|
|
258
259
|
//
|
|
259
|
-
//
|
|
260
|
+
// Paths are reported relative to the watched root. `shouldIgnoreWatchPath`
|
|
260
261
|
// (module-level, exported for tests) skips node_modules, .git, .webjs/, and
|
|
261
262
|
// the SQLite dev DB (db/dev.db) + db/migrations so a file the dev server itself writes never loops.
|
|
262
263
|
// Live-reload classification (#1398). Several files can change inside one
|
|
@@ -273,26 +274,32 @@ export async function startServer(opts) {
|
|
|
273
274
|
app.rebuild(v || undefined);
|
|
274
275
|
}, 80);
|
|
275
276
|
watcherAbort = new AbortController();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
// One warning per unwatchable path (#1521): an EACCES / ENOENT on a single
|
|
278
|
+
// file must never stop the dev server, and a burst of the same temp file
|
|
279
|
+
// must not flood the log either.
|
|
280
|
+
const warned = new Set();
|
|
281
|
+
const onWatchError = (err) => {
|
|
282
|
+
const key = `${err && err.code}:${err && err.path}`;
|
|
283
|
+
if (warned.has(key) || warned.size > 200) return;
|
|
284
|
+
warned.add(key);
|
|
285
|
+
logger.warn(`file watcher skipped ${err && err.path ? err.path : 'a path'} (${err && err.code ? err.code : String(err)})`);
|
|
286
|
+
};
|
|
287
|
+
const watchRoot = (root) => {
|
|
288
|
+
const close = watchTree(root, {
|
|
289
|
+
ignore: shouldIgnoreWatchPath,
|
|
290
|
+
onEvent: (filename) => {
|
|
282
291
|
// A regenerate output (#967) is a build product the server itself
|
|
283
292
|
// writes on request; ignoring it stops a spurious rebuild + reload
|
|
284
293
|
// (and a reload -> refetch -> recompile -> reload cycle), same as the
|
|
285
294
|
// db/dev.db carve-out above. `app` exposes the check because `state`
|
|
286
295
|
// lives in createRequestHandler's scope, not here.
|
|
287
|
-
if (app.isRegenerateOutput(filename))
|
|
296
|
+
if (app.isRegenerateOutput(filename)) return;
|
|
288
297
|
pendingVerdict = strongerVerdict(pendingVerdict, app.classifyWatchPath(filename, root));
|
|
289
298
|
rebuild();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
}
|
|
299
|
+
},
|
|
300
|
+
onError: onWatchError,
|
|
301
|
+
});
|
|
302
|
+
watcherAbort.signal.addEventListener('abort', close, { once: true });
|
|
296
303
|
};
|
|
297
304
|
watchRoot(app.appDir);
|
|
298
305
|
// Extra roots the app reads from OUTSIDE its appDir (#894): repo-root content
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { watch, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The dev live-reload watcher over one root (#1521).
|
|
6
|
+
*
|
|
7
|
+
* Two problems with the plain `fs.promises.watch(root, { recursive: true })`
|
|
8
|
+
* this replaces, both on Node 24's JS-implemented recursive watcher on Linux
|
|
9
|
+
* (`internal/fs/recursive_watch`), which opens one `fs.watch` per FILE:
|
|
10
|
+
*
|
|
11
|
+
* 1. A per-file failure (EACCES on a `sed -i` temp file another user created
|
|
12
|
+
* with mode 0600, ENOENT on a file that vanished between the directory event
|
|
13
|
+
* and the watch call) is emitted as `'error'` on an internal watcher the
|
|
14
|
+
* promise API never listens to, so it surfaces as an uncaughtException and
|
|
15
|
+
* the dev server shuts down. The callback form takes an `'error'` listener,
|
|
16
|
+
* and the watcher keeps delivering events for every other file after it.
|
|
17
|
+
* 2. Watching the app ROOT walks `node_modules` too: about 10k per-file watches
|
|
18
|
+
* and roughly 275ms of synchronous work on every boot of a scaffolded app,
|
|
19
|
+
* all for events the ignore filter then drops. So the root is watched
|
|
20
|
+
* non-recursively and each top-level directory that is not ignored gets its
|
|
21
|
+
* own recursive watcher.
|
|
22
|
+
*
|
|
23
|
+
* A top-level directory created or removed after start is picked up from the
|
|
24
|
+
* root watcher's events, so `mkdir lib` mid-session is watched like it was
|
|
25
|
+
* there at boot.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} root absolute directory to watch
|
|
28
|
+
* @param {{
|
|
29
|
+
* ignore: (relPath: string) => boolean,
|
|
30
|
+
* onEvent: (relPath: string) => void,
|
|
31
|
+
* onError: (err: NodeJS.ErrnoException) => void,
|
|
32
|
+
* }} opts `relPath` is relative to `root`, with OS separators.
|
|
33
|
+
* @returns {() => void} closes every watcher
|
|
34
|
+
*/
|
|
35
|
+
export function watchTree(root, { ignore, onEvent, onError }) {
|
|
36
|
+
/** @type {Map<string, import('node:fs').FSWatcher>} */
|
|
37
|
+
const dirs = new Map();
|
|
38
|
+
let closed = false;
|
|
39
|
+
|
|
40
|
+
const guard = (/** @type {import('node:fs').FSWatcher} */ w) => {
|
|
41
|
+
w.on('error', (err) => onError(/** @type {NodeJS.ErrnoException} */ (err)));
|
|
42
|
+
return w;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const isDir = (name) => {
|
|
46
|
+
try { return statSync(join(root, name)).isDirectory(); } catch { return false; }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const watchDir = (name) => {
|
|
50
|
+
if (closed || dirs.has(name) || ignore(name)) return;
|
|
51
|
+
try {
|
|
52
|
+
const w = guard(watch(join(root, name), { recursive: true }, (_type, filename) => {
|
|
53
|
+
const rel = filename ? join(name, String(filename)) : name;
|
|
54
|
+
if (!ignore(rel)) onEvent(rel);
|
|
55
|
+
}));
|
|
56
|
+
dirs.set(name, w);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
onError(/** @type {NodeJS.ErrnoException} */ (err));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @type {import('node:fs').FSWatcher | null} */
|
|
63
|
+
let top = null;
|
|
64
|
+
try {
|
|
65
|
+
top = guard(watch(root, (_type, filename) => {
|
|
66
|
+
if (!filename) return;
|
|
67
|
+
const name = String(filename);
|
|
68
|
+
if (ignore(name)) return;
|
|
69
|
+
// A top-level entry changed: start or stop following it as a directory.
|
|
70
|
+
if (isDir(name)) watchDir(name);
|
|
71
|
+
else if (dirs.has(name)) { try { dirs.get(name)?.close(); } catch {} dirs.delete(name); }
|
|
72
|
+
onEvent(name);
|
|
73
|
+
}));
|
|
74
|
+
} catch (err) {
|
|
75
|
+
onError(/** @type {NodeJS.ErrnoException} */ (err));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let entries = [];
|
|
79
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch {}
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
if (e.isDirectory() || (e.isSymbolicLink() && isDir(e.name))) watchDir(e.name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
closed = true;
|
|
86
|
+
try { top?.close(); } catch {}
|
|
87
|
+
for (const w of dirs.values()) { try { w.close(); } catch {} }
|
|
88
|
+
dirs.clear();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether an error came from a file watcher (#1521). Node marks every
|
|
94
|
+
* `fs.watch` failure with `syscall: 'watch'` (EACCES, EPERM, ENOENT, ELOOP, and
|
|
95
|
+
* ENOSPC when the inotify limit is reached). One unwatchable path is never a
|
|
96
|
+
* reason to stop a dev server, so the dev process handlers log these and keep
|
|
97
|
+
* serving.
|
|
98
|
+
*
|
|
99
|
+
* @param {unknown} err
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function isWatchError(err) {
|
|
103
|
+
return !!err && typeof err === 'object' && /** @type {any} */ (err).syscall === 'watch';
|
|
104
|
+
}
|
package/src/listener-bun.js
CHANGED
|
@@ -157,7 +157,7 @@ export function startBunListener(ctx) {
|
|
|
157
157
|
const shutdown = makeShutdown({ closeServer, hub, logger });
|
|
158
158
|
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
159
159
|
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
160
|
-
installProcessHandlers(logger, () => shutdown('uncaughtException', { fatal: true }));
|
|
160
|
+
installProcessHandlers(logger, () => shutdown('uncaughtException', { fatal: true }), { dev });
|
|
161
161
|
|
|
162
162
|
return {
|
|
163
163
|
server,
|
package/src/listener-core.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from 'node:zlib';
|
|
24
24
|
import { stripBasePath } from './base-path.js';
|
|
25
25
|
import { randomUUID } from 'node:crypto';
|
|
26
|
+
import { isWatchError } from './dev/watch-tree.js';
|
|
26
27
|
|
|
27
28
|
/** The dev live-reload SSE path (matched after base-path stripping). */
|
|
28
29
|
export const EVENTS_PATH = '/__webjs/events';
|
|
@@ -416,10 +417,18 @@ export class SseHub {
|
|
|
416
417
|
/**
|
|
417
418
|
* Install once-only process error handlers. Idempotent across multiple
|
|
418
419
|
* `startServer` calls in the same process. Runtime-neutral (plain `process.on`).
|
|
420
|
+
*
|
|
421
|
+
* In dev, an uncaught file-watcher error (`syscall: 'watch'`, #1521) is logged
|
|
422
|
+
* as a warning and NOT fatal: one unreadable or vanished file in a watched dir
|
|
423
|
+
* says nothing about the server's state, and shutting down over it is how a
|
|
424
|
+
* `sed -i` from another user took a whole dev preview down. Prod keeps the
|
|
425
|
+
* strict contract, every uncaught exception is fatal.
|
|
426
|
+
*
|
|
419
427
|
* @param {import('./logger.js').Logger} logger
|
|
420
428
|
* @param {() => void} onFatal Starts the FATAL shutdown, which exits non-zero.
|
|
429
|
+
* @param {{ dev?: boolean }} [opts]
|
|
421
430
|
*/
|
|
422
|
-
export function installProcessHandlers(logger, onFatal) {
|
|
431
|
+
export function installProcessHandlers(logger, onFatal, { dev = false } = {}) {
|
|
423
432
|
if (/** @type any */ (globalThis).__webjsProcHandlers) return;
|
|
424
433
|
/** @type any */ (globalThis).__webjsProcHandlers = true;
|
|
425
434
|
process.on('unhandledRejection', (reason) => {
|
|
@@ -428,6 +437,10 @@ export function installProcessHandlers(logger, onFatal) {
|
|
|
428
437
|
});
|
|
429
438
|
});
|
|
430
439
|
process.on('uncaughtException', (err) => {
|
|
440
|
+
if (dev && isWatchError(err)) {
|
|
441
|
+
logger.warn(`file watcher skipped ${/** @type any */ (err).path || 'a path'} (${/** @type any */ (err).code || err.message})`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
431
444
|
logger.error('uncaughtException', { err: err.stack || err.message });
|
|
432
445
|
// Begin orderly shutdown; process state may be corrupt.
|
|
433
446
|
try { onFatal(); } catch {}
|