-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwatch.ts
127 lines (116 loc) · 3.04 KB
/
watch.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
119
120
121
122
123
124
125
126
127
import {
call,
createChannel,
createSignal,
Err,
Ok,
type Operation,
resource,
type Result,
spawn,
type Stream,
} from "effection";
import { pipe } from "jsr:@gordonb/pipe";
import chokidar, { type EmitArgsWithName } from "chokidar";
import { default as createIgnore } from "ignore";
import { debounce, filter } from "./stream-helpers.ts";
import { type Process, useProcess } from "./child-process.ts";
import { exists } from "@std/fs";
import { join, relative } from "@std/path";
import { readFile } from "node:fs/promises";
export interface Watch extends Stream<Result<Process>, never> {}
/**
* Options available to configure what is watched
*/
export interface WatchOptions {
/**
* The directory to watch
*/
path: string;
/**
* The command to run (and re-run every time a change is detected)
*/
cmd: string;
/**
* @ignore
*/
event?: "all" | "change";
}
/**
* Create a watch configuration that can be consumed as
* a stream of process starts.
*/
export function watch(options: WatchOptions): Watch {
return resource(function* (provide) {
let starts = createChannel<Result<Process>, never>();
let input = createSignal<EmitArgsWithName, never>();
let watcher = chokidar.watch(options.path);
let { event = "all" } = options;
watcher.on(event, (...args: EmitArgsWithName) => {
if (event !== "all") {
args.unshift(event);
}
input.send(args);
});
let ignores = yield* findIgnores(options.path);
let changes = yield* pipe(
input,
fresh(500),
ignores,
debounce(100),
);
yield* spawn(function* () {
while (true) {
let task = yield* spawn(function* () {
try {
let process = yield* useProcess(options.cmd);
yield* starts.send(Ok(process));
} catch (error) {
yield* starts.send(Err(error as Error));
}
yield* changes.next();
});
yield* task;
}
});
try {
yield* provide(yield* starts);
} finally {
yield* call(() => watcher.close());
}
});
}
/**
* locate a `.gitignore` file if it exists and use it to filter
* out any change events against paths that are matched by it
*/
function* findIgnores(
path: string,
): Operation<
<R>(stream: Stream<EmitArgsWithName, R>) => Stream<EmitArgsWithName, R>
> {
let gitignore = join(path, ".gitignore");
if (yield* call(() => exists(gitignore))) {
let ignores = createIgnore();
let buffer = yield* call(() => readFile(gitignore));
ignores.add(buffer.toString());
return filter(([, pathname]) => {
return !pathname.startsWith(".git") && !ignores.ignores(relative(path, pathname));
});
} else {
return filter(() => true);
}
}
function fresh<R>(
staletime: number,
): (stream: Stream<EmitArgsWithName, R>) => Stream<EmitArgsWithName, R> {
return filter(([,path,stats]) => {
if (stats) {
let ageMs = Date.now() - stats.atimeMs;
return ageMs < staletime;
} else {
console.log({ path })
return true;
}
})
}