@jxsuite/server 2.2.2 → 2.2.3
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/refactor/watcher.ts +16 -1
- package/src/watch-policy.ts +79 -0
- package/src/watch.ts +5 -1
package/package.json
CHANGED
package/src/refactor/watcher.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { watch as chokidarWatch } from "chokidar";
|
|
10
10
|
import { coalesceFsEvents, toFsEvent } from "./fs-events.ts";
|
|
11
11
|
import { invalidateReferenceCache } from "./find-refs.ts";
|
|
12
|
+
import { createWatchIgnore } from "../watch-policy.ts";
|
|
12
13
|
import type { FsEventPayload } from "./fs-events.ts";
|
|
13
14
|
|
|
14
15
|
const IGNORE_SEGMENTS = ["node_modules", ".git", "dist", ".jx", ".direnv", ".devenv"];
|
|
@@ -36,9 +37,14 @@ export function createFsWatcher(
|
|
|
36
37
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
37
38
|
|
|
38
39
|
const watcher = chokidarWatch(root, {
|
|
40
|
+
/* Left on deliberately: a symlink inside a project is project content, and its events belong
|
|
41
|
+
in the sidebar. It is the link pointing OUT of the root that walks the watcher off into the
|
|
42
|
+
rest of the filesystem, and createWatchIgnore drops exactly those — along with the sockets
|
|
43
|
+
and device nodes fs.watch answers with ENXIO. */
|
|
44
|
+
followSymlinks: true,
|
|
39
45
|
ignoreInitial: true,
|
|
40
46
|
ignorePermissionErrors: true,
|
|
41
|
-
ignored: (
|
|
47
|
+
ignored: createWatchIgnore(root, isIgnored),
|
|
42
48
|
});
|
|
43
49
|
|
|
44
50
|
watcher.on("all", (eventType, changedPath) => {
|
|
@@ -61,6 +67,15 @@ export function createFsWatcher(
|
|
|
61
67
|
}, debounceMs);
|
|
62
68
|
});
|
|
63
69
|
|
|
70
|
+
/* An `error` with no listener is not logged by an EventEmitter, it is THROWN — which is how a
|
|
71
|
+
directory holding a few unix sockets took the whole launcher down rather than losing a few
|
|
72
|
+
watches. The filter above means these should now be rare; the listener means they are never
|
|
73
|
+
fatal when they are not. */
|
|
74
|
+
watcher.on("error", (err) => {
|
|
75
|
+
const error = err as NodeJS.ErrnoException;
|
|
76
|
+
console.error(`[fs-watch] ${error.message ?? String(error)}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
64
79
|
return {
|
|
65
80
|
close: async () => {
|
|
66
81
|
if (timer) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch-policy.ts — the entry filter both filesystem watchers hand to chokidar.
|
|
3
|
+
*
|
|
4
|
+
* A watcher is pointed at a directory a **user** chose, and a user's directory holds things that
|
|
5
|
+
* are not project content and that `fs.watch` does not merely decline to watch:
|
|
6
|
+
*
|
|
7
|
+
* - **Unix sockets, FIFOs and device nodes** answer `fs.watch` with `ENXIO`, which chokidar turns
|
|
8
|
+
* into an `error` event — one per entry, and fatal to a watcher with no `error` listener.
|
|
9
|
+
* - **A symlink out of the tree** is followed by default, so one link can turn a project watcher into
|
|
10
|
+
* a walk of the whole filesystem. `~/.wine/dosdevices/z:` points at `/`, and that is exactly what
|
|
11
|
+
* it did.
|
|
12
|
+
*
|
|
13
|
+
* Both were reached at once by a launcher that adopted `$HOME` as a project root. That defect is
|
|
14
|
+
* fixed where it was made (`packages/desktop/src/chromium/index.ts`), but a watcher must survive
|
|
15
|
+
* being pointed at an ordinary directory rather than depend on never being pointed at one — so the
|
|
16
|
+
* rules live here, stated once, because `watch.ts` (dev server) and `refactor/watcher.ts` (desktop
|
|
17
|
+
* session) both need them and neither owns the other.
|
|
18
|
+
*
|
|
19
|
+
* Symlinks are contained rather than banned: a link resolving back inside the root is project
|
|
20
|
+
* content (a workspace member, a shared assets directory) and keeps its events. Only one leaving
|
|
21
|
+
* the root — or dangling, which is what `find -xtype l -delete` leaves behind in a nix bundle — is
|
|
22
|
+
* dropped.
|
|
23
|
+
*
|
|
24
|
+
* @docs framework/build/dev-server
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { realpathSync } from "node:fs";
|
|
28
|
+
import { resolve, sep } from "node:path";
|
|
29
|
+
import type { Stats } from "node:fs";
|
|
30
|
+
|
|
31
|
+
/** Is `candidate` the base directory itself, or somewhere beneath it? */
|
|
32
|
+
function isInside(base: string, candidate: string): boolean {
|
|
33
|
+
return candidate === base || candidate.startsWith(base.endsWith(sep) ? base : base + sep);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve `root` through any symlinks so containment is judged against real paths on both sides. An
|
|
38
|
+
* unresolvable root is not this function's error to raise — chokidar reports it — so the lexical
|
|
39
|
+
* path stands in and every symlink under it then reads as "outside".
|
|
40
|
+
*/
|
|
41
|
+
function realBase(root: string): string {
|
|
42
|
+
try {
|
|
43
|
+
return realpathSync(root);
|
|
44
|
+
} catch {
|
|
45
|
+
return resolve(root);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the `ignored` predicate for a watcher rooted at `root`, composing the caller's own
|
|
51
|
+
* path-name rule (`node_modules`, `dist`, …) with the entry-kind rules above.
|
|
52
|
+
*
|
|
53
|
+
* Chokidar calls this with the entry's `lstat` during its directory walk and without stats before
|
|
54
|
+
* it has them; a stats-less call can only be judged by name, so it is.
|
|
55
|
+
*/
|
|
56
|
+
export function createWatchIgnore(
|
|
57
|
+
root: string,
|
|
58
|
+
isIgnoredPath: (path: string) => boolean,
|
|
59
|
+
): (path: string, stats?: Stats) => boolean {
|
|
60
|
+
let base: string | null = null;
|
|
61
|
+
return (path: string, stats?: Stats): boolean => {
|
|
62
|
+
if (isIgnoredPath(path)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (!stats) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (stats.isSymbolicLink()) {
|
|
69
|
+
base ??= realBase(root);
|
|
70
|
+
try {
|
|
71
|
+
return !isInside(base, realpathSync(path));
|
|
72
|
+
} catch {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Anything that is not a directory or a regular file is a socket, a FIFO or a device node.
|
|
77
|
+
return !stats.isDirectory() && !stats.isFile();
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/watch.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { watch as chokidarWatch } from "chokidar";
|
|
|
4
4
|
import { relative } from "node:path";
|
|
5
5
|
import { rebuild } from "./build.ts";
|
|
6
6
|
import { coalesceFsEvents, toFsEvent } from "./refactor/fs-events.ts";
|
|
7
|
+
import { createWatchIgnore } from "./watch-policy.ts";
|
|
7
8
|
import { invalidateReferenceCache } from "./refactor/find-refs.ts";
|
|
8
9
|
import type { BuildEntry } from "./types.ts";
|
|
9
10
|
import type { FsEventPayload } from "./refactor/fs-events.ts";
|
|
@@ -201,9 +202,12 @@ export function createWatcher(
|
|
|
201
202
|
pollInterval: 10,
|
|
202
203
|
stabilityThreshold: debounceMs,
|
|
203
204
|
},
|
|
205
|
+
/* See watch-policy.ts: `ignore` is a name rule and cannot see that an entry is a unix socket
|
|
206
|
+
(ENXIO from fs.watch) or a symlink pointing out of the project (a walk of the filesystem). */
|
|
207
|
+
followSymlinks: true,
|
|
204
208
|
ignoreInitial: true,
|
|
205
209
|
ignorePermissionErrors: true,
|
|
206
|
-
ignored: (watchedPath) => shouldIgnore(watchedPath, ignore),
|
|
210
|
+
ignored: createWatchIgnore(root, (watchedPath) => shouldIgnore(watchedPath, ignore)),
|
|
207
211
|
});
|
|
208
212
|
|
|
209
213
|
watcher.on("all", (eventType, changedPath) => {
|