forked from denosaurs/denon
-
Notifications
You must be signed in to change notification settings - Fork 0
/
watcher.ts
118 lines (107 loc) · 3.11 KB
/
watcher.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { deferred, globToRegExp, resolve } from "./deps.ts";
type FileEvent = "any" | "access" | "create" | "modify" | "remove";
/** A file that was changed, created or removed */
export interface FileChange {
/** The path of the changed file */
path: string;
/** The type of change that occurred */
event: FileEvent;
}
/** All of the options for the `watch` generator */
export interface WatchOptions {
/** The number of milliseconds after the last change */
interval?: number;
/** Scan for files if in folders of `paths` */
recursive?: boolean;
/** The file extensions that it will scan for */
exts?: string[];
/** The globs that it will scan for */
match?: string[];
/** The globs that it will not scan for */
skip?: string[];
}
/**
* Watches for file changes in `paths` path yielding an array of all of the changes
* each time one or more changes are detected. It is debounced by `interval`.
* `recursive`, `exts`, `match` and `skip` are filtering the files wich will yield a change
*/
export default class Watcher implements AsyncIterable<FileChange[]> {
// private events: AsyncIterableIterator<Deno.FsEvent>;
private signal = deferred();
private changes: { [key: string]: FileEvent } = {};
private interval: number;
private exts?: string[];
private match?: RegExp[];
private skip?: RegExp[];
private recursive: boolean;
private paths: string[];
constructor(
paths: string[],
{
interval = 500,
recursive = true,
exts = undefined,
match = undefined,
skip = undefined,
}: WatchOptions = {},
) {
this.paths = paths.map((p) => resolve(p));
this.interval = interval;
this.exts = exts?.map((e) => e.startsWith(".") ? e : `.${e}`);
this.match = match?.map((s) =>
globToRegExp(s, { extended: true, globstar: false })
);
this.skip = skip?.map((s) =>
globToRegExp(s, { extended: true, globstar: false })
);
this.recursive = recursive ?? true;
}
reset() {
this.changes = {};
this.signal = deferred();
}
isWatched(
path: string,
): boolean {
if (this.exts?.every((ext) => !path.endsWith(ext))) {
return false;
} else if (this.skip?.some((skip) => path.match(skip))) {
return false;
} else if (this.match?.every((match) => !path.match(match))) {
return false;
}
return true;
}
async watch() {
let timer = 0;
const debounce = () => {
clearTimeout(timer);
timer = setTimeout(this.signal.resolve, this.interval);
};
for await (
const event of Deno.fsEvents(this.paths, { recursive: this.recursive })
) {
const { kind, paths } = event;
paths.forEach((path) => {
if (this.isWatched(path)) {
this.changes[path] = kind;
debounce();
}
});
}
}
async *iterate(): AsyncIterator<FileChange[]> {
this.watch();
while (true) {
await this.signal;
yield Object.entries(this.changes).map(([
path,
event,
]) => ({ path, event }));
this.reset();
}
}
[Symbol.asyncIterator](): AsyncIterator<FileChange[]> {
return this.iterate();
}
}