@dbx-tools/projen 0.6.13 → 0.6.14
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/watch.ts +55 -8
package/package.json
CHANGED
package/src/watch.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `watchLoop` wraps `@dbx-tools/path`'s chokidar watcher with the
|
|
5
5
|
* behavior every dbx-tools watcher wants: it debounces bursts, serializes runs (a
|
|
6
6
|
* change mid-run re-runs once afterwards), drops generated paths (barrels/manifests/
|
|
7
|
-
* decls - reacting to our own output would loop),
|
|
7
|
+
* decls - reacting to our own output would loop), exits on a watcher error chokidar
|
|
8
|
+
* cannot recover from ({@link FATAL_WATCH_ERROR_CODES}), and shuts down on SIGINT. Callers
|
|
8
9
|
* pass the paths to watch and an `onBatch` handler; the concern-specific glue - which
|
|
9
10
|
* barrels to rebuild, when to regenerate openapi, when to re-synth - lives in the task
|
|
10
11
|
* that owns it (`tasks/barrels.ts`, `tasks/openapi.ts`, `tasks/projenrc.ts`), each
|
|
@@ -16,12 +17,36 @@
|
|
|
16
17
|
*/
|
|
17
18
|
import { isAbsolute, resolve } from "node:path";
|
|
18
19
|
import { watch as fileScan } from "@dbx-tools/path";
|
|
19
|
-
import { log } from "@dbx-tools/shared-core";
|
|
20
|
+
import { async, log } from "@dbx-tools/shared-core";
|
|
20
21
|
import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
|
|
21
22
|
|
|
22
23
|
const logger = log.logger("projen:watch");
|
|
23
24
|
const DEBOUNCE_MS = 250;
|
|
24
25
|
|
|
26
|
+
/** How long to let chokidar release its watches before exiting regardless. */
|
|
27
|
+
const CLOSE_GRACE_MS = 2_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Watcher errors no amount of waiting recovers from: the process is out of file
|
|
31
|
+
* descriptors (`EMFILE`/`ENFILE`) or the kernel watch table is full (`ENOSPC`).
|
|
32
|
+
*
|
|
33
|
+
* These have to end the process, because chokidar only EMITS them - it never closes
|
|
34
|
+
* itself or marks the instance dead (its own source carries a
|
|
35
|
+
* `TODO: emit errors properly. Example: EMFILE on Macos.`). A handler that just logs
|
|
36
|
+
* therefore leaves the watcher open, still holding every descriptor it took, while
|
|
37
|
+
* silently delivering no further events; and since the process stays alive, `sync`'s
|
|
38
|
+
* `restartTries: -1` supervisor never sees an exit to respawn. The watcher looks
|
|
39
|
+
* healthy and its outputs quietly go stale. Exiting non-zero instead hands the
|
|
40
|
+
* restart to `concurrently`, which is what makes a transient exhaustion recover on
|
|
41
|
+
* its own and a persistent one loud (a respawn every `RESTART_DELAY_MS`).
|
|
42
|
+
*
|
|
43
|
+
* Every other error is left as a logged warning: chokidar has already filtered the
|
|
44
|
+
* benign `ENOENT`/`ENOTDIR` cases, and what remains (an unreadable path, say) costs
|
|
45
|
+
* that one path while the rest of the tree keeps working - a restart would only hit
|
|
46
|
+
* it again.
|
|
47
|
+
*/
|
|
48
|
+
const FATAL_WATCH_ERROR_CODES = new Set(["EMFILE", "ENFILE", "ENOSPC"]);
|
|
49
|
+
|
|
25
50
|
/** node-path's built-in ignore-group toggles (`{ dot, temp, test, lock, defaults }`). */
|
|
26
51
|
export type IgnoreGroupOptions = NonNullable<
|
|
27
52
|
Parameters<typeof fileScan.watchFiles>[1]
|
|
@@ -45,7 +70,11 @@ function ignoredPath(path: string): boolean {
|
|
|
45
70
|
* Shared debounce/flush machinery backed by `watchFiles`. Watches `paths` and, on
|
|
46
71
|
* each debounced batch of non-generated changes, calls `onBatch` with the absolute
|
|
47
72
|
* changed paths. Runs are serialized (a change during a run re-runs once afterwards);
|
|
48
|
-
* watches until SIGINT
|
|
73
|
+
* watches until SIGINT, or until a {@link FATAL_WATCH_ERROR_CODES} error makes the
|
|
74
|
+
* watcher blind, which exits non-zero so `sync` respawns it.
|
|
75
|
+
*
|
|
76
|
+
* A failing `onBatch` is NOT fatal - the offending edit is usually the one the next
|
|
77
|
+
* save fixes - so it is logged and the watch continues.
|
|
49
78
|
*
|
|
50
79
|
* `ignoreOptions` toggles node-path's built-in ignore groups for this watcher only
|
|
51
80
|
* (e.g. the projenrc watcher passes `{ dot: false }` so its lone dotfile target,
|
|
@@ -92,16 +121,34 @@ export function watchLoop(
|
|
|
92
121
|
ignore: (path) => ignoredPath(path),
|
|
93
122
|
ignoreOptions,
|
|
94
123
|
});
|
|
124
|
+
let closing = false;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Release the watches and leave with `code`. Closing is raced against
|
|
128
|
+
* {@link CLOSE_GRACE_MS} so a wedged chokidar cannot hold up a Ctrl-C, and exiting
|
|
129
|
+
* reclaims the descriptors either way.
|
|
130
|
+
*/
|
|
131
|
+
async function shutdown(code: number): Promise<void> {
|
|
132
|
+
if (closing) return;
|
|
133
|
+
closing = true;
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
await Promise.race([watcher.close().catch(() => {}), async.sleep(CLOSE_GRACE_MS)]);
|
|
136
|
+
process.exit(code);
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
watcher.on("all", (_event, path) => {
|
|
96
140
|
pending.add(path);
|
|
97
141
|
clearTimeout(timer);
|
|
98
142
|
timer = setTimeout(() => void flush(), DEBOUNCE_MS);
|
|
99
143
|
});
|
|
100
|
-
watcher.on("error", (err) =>
|
|
144
|
+
watcher.on("error", (err) => {
|
|
145
|
+
logger.error(`${tag} watcher error:`, err);
|
|
146
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
147
|
+
if (code === undefined || !FATAL_WATCH_ERROR_CODES.has(code)) return;
|
|
148
|
+
logger.error(`${tag}: ${code} leaves the watcher blind - exiting so it is restarted`);
|
|
149
|
+
void shutdown(1);
|
|
150
|
+
});
|
|
101
151
|
watcher.on("ready", () => logger.info(`${tag}: watching for changes … (Ctrl-C to stop)`));
|
|
102
152
|
|
|
103
|
-
process.on("SIGINT", () =>
|
|
104
|
-
void watcher.close();
|
|
105
|
-
process.exit(0);
|
|
106
|
-
});
|
|
153
|
+
process.on("SIGINT", () => void shutdown(0));
|
|
107
154
|
}
|