@jxsuite/server 2.2.2 → 2.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,13 +39,13 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@jxsuite/collab": "^0.8.1",
42
- "@jxsuite/compiler": "^2.0.3",
43
- "@jxsuite/create": "^1.3.3",
44
- "@jxsuite/import": "^0.39.3",
45
- "@jxsuite/protocol": "^1.1.2",
46
- "@jxsuite/runtime": "^2.0.2",
42
+ "@jxsuite/compiler": "^2.0.4",
43
+ "@jxsuite/create": "^1.3.4",
44
+ "@jxsuite/import": "^0.39.4",
45
+ "@jxsuite/protocol": "^1.2.0",
46
+ "@jxsuite/runtime": "^2.0.3",
47
47
  "@jxsuite/schema": "^1.8.0",
48
- "@jxsuite/starters": "^1.6.0",
48
+ "@jxsuite/starters": "^1.6.1",
49
49
  "chokidar": "^5.0.0",
50
50
  "kysely": "^0.29.5",
51
51
  "zod": "^4.4.3"
@@ -53,6 +53,6 @@
53
53
  "devDependencies": {
54
54
  "@jxsuite/auth": "^0.5.2",
55
55
  "@jxsuite/connector": "^0.5.2",
56
- "@jxsuite/parser": "^1.5.2"
56
+ "@jxsuite/parser": "^1.5.3"
57
57
  }
58
58
  }
@@ -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: (path) => isIgnored(path),
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) => {