@dbx-tools/path 0.9.119 → 0.9.120

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/src/watch.ts CHANGED
@@ -10,14 +10,19 @@
10
10
  */
11
11
 
12
12
  import { Stats } from "fs";
13
+ import { readFile } from "node:fs/promises";
13
14
  import { isAbsolute, relative, resolve } from "path";
14
- import { ChokidarOptions, MatchFunction, watch } from "chokidar";
15
+ import { errorUtils, log } from "@dbx-tools/shared-core";
16
+ import { ChokidarOptions, MatchFunction, watch, type FSWatcher } from "chokidar";
15
17
  import { hasMagic } from "glob";
16
18
  import { findFiles, type FileFindOptions } from "./find.ts";
19
+ import { GitIgnore } from "./git-ignore.ts";
17
20
  import { ignorePathMatcher } from "./ignore.ts";
18
21
  import { pathMatchTests } from "./match.ts";
19
22
  import { FileScanIgnoreOptions, FileScanOptions, FOLLOW_SYMLINKS_DEFAULT } from "./scan.ts";
20
23
 
24
+ const logger = log.logger("path/watch");
25
+
21
26
  /** Chokidar options extended with the shared dbx-tools ignore configuration. */
22
27
  export interface FileWatchOptions
23
28
  extends Omit<ChokidarOptions, "ignored">, Omit<FileScanOptions, "ignore"> {
@@ -25,6 +30,7 @@ export interface FileWatchOptions
25
30
  }
26
31
 
27
32
  interface NormalizedFileWatchOptions extends Omit<FileWatchOptions, "ignore" | "ignoreOptions"> {
33
+ gitIgnore?: GitIgnore;
28
34
  ignored: MatchFunction;
29
35
  }
30
36
 
@@ -33,15 +39,18 @@ interface NormalizedFileWatchOptions extends Omit<FileWatchOptions, "ignore" | "
33
39
  * ignore groups so it prunes the same paths {@link findFiles} skips for the
34
40
  * same patterns.
35
41
  */
36
- export function watchFiles(paths: string | string[], options?: FileWatchOptions) {
37
- const normalized = toNormalizedFileWatchOptions(options);
38
- const { ignored, cwd, followSymlinks, ...chokidarOptions } = normalized;
42
+ export async function watchFiles(
43
+ paths: string | string[],
44
+ options?: FileWatchOptions,
45
+ ): Promise<FSWatcher> {
46
+ const normalized = await toNormalizedFileWatchOptions(options);
47
+ const { gitIgnore, ignored, cwd, followSymlinks, ...chokidarOptions } = normalized;
39
48
  const { paths: literals, globs } = partitionPaths(paths);
40
49
 
41
50
  let watchPaths: string[];
42
51
  if (globs.length > 0) {
43
52
  const distinctWatchPaths = new Set(literals);
44
- for (const path of findFiles(globs, toFindFilesOptions(normalized))) {
53
+ for await (const path of findFiles(globs, toFindFilesOptions(normalized))) {
45
54
  distinctWatchPaths.add(path);
46
55
  }
47
56
  watchPaths = [...distinctWatchPaths];
@@ -49,12 +58,21 @@ export function watchFiles(paths: string | string[], options?: FileWatchOptions)
49
58
  watchPaths = literals;
50
59
  }
51
60
 
52
- return watch(watchPaths, {
61
+ const watcher = watch(watchPaths, {
53
62
  ...chokidarOptions,
54
63
  cwd,
55
64
  followSymlinks,
56
65
  ignored,
57
66
  });
67
+ if (gitIgnore) {
68
+ attachGitIgnoreWatcher(watcher, gitIgnore, watchPaths, {
69
+ cwd,
70
+ followSymlinks,
71
+ ignoreOptions: options?.ignoreOptions,
72
+ persistent: chokidarOptions.persistent,
73
+ });
74
+ }
75
+ return watcher;
58
76
  }
59
77
 
60
78
  function partitionPaths(paths: string | string[]): { paths: string[]; globs: string[] } {
@@ -71,16 +89,23 @@ function partitionPaths(paths: string | string[]): { paths: string[]; globs: str
71
89
  * built-in groups are consolidated into one chokidar {@link MatchFunction}. When
72
90
  * `stats` marks a directory, the matcher also tests `path + "/"` for pruning.
73
91
  */
74
- function toNormalizedFileWatchOptions(options?: FileWatchOptions): NormalizedFileWatchOptions & {
75
- cwd: string;
76
- followSymlinks: boolean;
77
- } {
92
+ async function toNormalizedFileWatchOptions(options?: FileWatchOptions): Promise<
93
+ NormalizedFileWatchOptions & {
94
+ cwd: string;
95
+ followSymlinks: boolean;
96
+ }
97
+ > {
78
98
  const { cwd, ignore, followSymlinks, ignoreOptions, ...chokidarOptions } = options ?? {};
99
+ const root = cwd ?? process.cwd();
100
+ const gitIgnore = ignoreOptions?.git
101
+ ? await GitIgnore.load(root, { ignoreOptions, followSymlinks })
102
+ : undefined;
79
103
  return {
80
104
  ...chokidarOptions,
81
- cwd: cwd ?? process.cwd(),
105
+ cwd: root,
82
106
  followSymlinks: followSymlinks ?? FOLLOW_SYMLINKS_DEFAULT,
83
- ignored: toWatchMatchFunction(ignore, ignoreOptions, cwd ?? process.cwd()),
107
+ ...(gitIgnore ? { gitIgnore } : {}),
108
+ ignored: toWatchMatchFunction(ignore, ignoreOptions, root, gitIgnore),
84
109
  };
85
110
  }
86
111
 
@@ -105,6 +130,7 @@ function toWatchMatchFunction(
105
130
  ignore: FileWatchOptions["ignore"] | undefined,
106
131
  ignoreOptions: FileScanIgnoreOptions | undefined,
107
132
  cwd: string,
133
+ gitIgnore?: GitIgnore,
108
134
  ): MatchFunction {
109
135
  const patterns: string[] = [];
110
136
  const functions: MatchFunction[] = [];
@@ -121,6 +147,7 @@ function toWatchMatchFunction(
121
147
 
122
148
  const matchPath = (path: string, stats?: Stats): boolean => {
123
149
  const rel = toCwdRelativePath(path, cwd);
150
+ if (gitIgnore?.ignores(rel, stats?.isDirectory() ?? false)) return true;
124
151
  if (matcher(rel)) return true;
125
152
  for (const fn of functions) {
126
153
  if (fn(rel, stats)) return true;
@@ -138,6 +165,59 @@ function toWatchMatchFunction(
138
165
  return matchPath;
139
166
  }
140
167
 
168
+ function attachGitIgnoreWatcher(
169
+ watcher: FSWatcher,
170
+ gitIgnore: GitIgnore,
171
+ watchPaths: readonly string[],
172
+ options: {
173
+ cwd: string;
174
+ followSymlinks: boolean;
175
+ ignoreOptions?: FileScanIgnoreOptions;
176
+ persistent?: boolean;
177
+ },
178
+ ): void {
179
+ const builtInIgnore = ignorePathMatcher(options.ignoreOptions);
180
+ const controls = watch(".", {
181
+ atomic: false,
182
+ cwd: options.cwd,
183
+ followSymlinks: options.followSymlinks,
184
+ ignoreInitial: true,
185
+ persistent: options.persistent,
186
+ ignored(path, stats) {
187
+ const relativePath = toCwdRelativePath(path, options.cwd);
188
+ if (GitIgnore.isFile(relativePath)) return false;
189
+ if (builtInIgnore(relativePath) || builtInIgnore(`${relativePath}/`)) return true;
190
+ return stats ? !stats.isDirectory() : false;
191
+ },
192
+ });
193
+ const reload = (path: string): void => {
194
+ void readFile(resolve(options.cwd, path), "utf8")
195
+ .then((content) => {
196
+ gitIgnore.set(path, content);
197
+ watcher.add([...watchPaths]);
198
+ logger.debug(".gitignore updated", { path });
199
+ })
200
+ .catch((error) => {
201
+ logger.warn("failed to reload .gitignore", {
202
+ path,
203
+ error: errorUtils.toError(error),
204
+ });
205
+ });
206
+ };
207
+ controls.on("add", reload);
208
+ controls.on("change", reload);
209
+ controls.on("unlink", (path) => {
210
+ gitIgnore.delete(path);
211
+ watcher.add([...watchPaths]);
212
+ logger.debug(".gitignore removed", { path });
213
+ });
214
+ controls.on("error", (error) => watcher.emit("error", error));
215
+ const close = watcher.close.bind(watcher);
216
+ watcher.close = async () => {
217
+ await Promise.all([close(), controls.close()]);
218
+ };
219
+ }
220
+
141
221
  /** Aligns chokidar paths with the cwd-relative form {@link findFiles} uses. */
142
222
  function toCwdRelativePath(path: string, cwd: string): string {
143
223
  if (!isAbsolute(path)) return path;
@@ -152,7 +232,7 @@ if (import.meta.main) {
152
232
  console.log("--- Starting File Watcher ---");
153
233
  console.log(`Working Directory: ${cwd}`);
154
234
 
155
- const watcher = watchFiles("**/src/**", {
235
+ const watcher = await watchFiles("**/src/**", {
156
236
  cwd,
157
237
  ignoreInitial: false,
158
238
  ignore: ["**/index.ts", "**/core/**"],