Skip to content

Commit d82307b

Browse files
authored
Merge pull request #59 from thefrontside/tm/import-watch
I'm commenting out the test on linux until the underlying issue with Deno can be resolved.
2 parents a3207d2 + dc058e4 commit d82307b

File tree

17 files changed

+830
-6
lines changed

17 files changed

+830
-6
lines changed

.github/workflows/verify.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ jobs:
2828

2929
- run: deno lint
3030

31-
- run: deno test --allow-net --allow-read --allow-write
31+
- run: deno test --allow-net --allow-read --allow-write --allow-env --allow-run

deno.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
22
"imports": {
3-
"effection": "npm:effection@3.0.3",
4-
"revolution": "https://deno.land/x/revolution@0.6.0/mod.ts",
5-
"revolution/jsx-runtime": "https://deno.land/x/revolution@0.6.0/jsx-runtime.ts",
63
"bdd": "jsr:@std/testing/bdd",
74
"expect": "jsr:@std/expect"
85
},
@@ -31,6 +28,8 @@
3128
"./test-adapter",
3229
"./tinyexec",
3330
"./timebox",
31+
"./tinyexec",
32+
"./watch",
3433
"./websocket",
3534
"./worker"
3635
]

tasks/lib/read-packages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { call, type Operation } from "effection";
1+
import { call, type Operation } from "npm:effection@3.2.1";
22
import { resolve } from "jsr:@std/path@^1.0.6";
33
import { z } from "npm:zod@3.23.8";
44

tasks/publish-matrix.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { call, main } from "effection";
1+
import { call, main } from "npm:effection@3.2.1";
22
import { x } from "../tinyexec/mod.ts";
33
import { readPackages } from "./lib/read-packages.ts";
44

watch/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/test/temp/
2+
*.tmp-*
3+
watch

watch/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# @effection-contrib/watch
2+
3+
Watch is a very simple tool that does one thing: run a command, and every time
4+
source files change in a directory, shutdown the current invocation _gracefully_
5+
and restart it.
6+
7+
```
8+
deno -A jsr:@effection-contrib/watch npm start
9+
```
10+
11+
## Graceful Shutdown
12+
13+
Watch will send SIGINT and SIGTERM to your command, and then wait until its
14+
`stdout` stream is closed, indicating that it has no further output. It will not
15+
attempt to start your command again until that has happened. This is important,
16+
because your process might be holding onto any number of resources that have to
17+
be safely released before exiting.
18+
19+
## Git aware
20+
21+
If you are running this command inside a git repository, it will only perform
22+
restarts on files that are under source control, or could be candidates for
23+
source control (not ignored).
24+
25+
## Use it as a library
26+
27+
```ts
28+
import { main } from "effection";
29+
import { watch } from "@effection-contrib/watch";
30+
31+
await main(function* () {
32+
const watcher = yield* watch({
33+
path: "./src",
34+
cmd: "npm test",
35+
});
36+
});
37+
```

watch/child-process.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { spawn as nodeSpawn } from "node:child_process";
2+
import type { Signals } from "npm:@types/node@16.18.126";
3+
4+
import type { Operation, Stream } from "effection";
5+
import {
6+
action,
7+
createSignal,
8+
resource,
9+
spawn,
10+
withResolvers,
11+
} from "effection";
12+
13+
export interface ProcessResult {
14+
code: number;
15+
signal?: Signals;
16+
}
17+
18+
export interface Process extends Operation<ProcessResult> {
19+
stdout: Stream<string, void>;
20+
stderr: Stream<string, void>;
21+
send(signal: Signals): void;
22+
}
23+
24+
export function useProcess(command: string): Operation<Process> {
25+
return resource(function* (provide) {
26+
let closed = withResolvers<ProcessResult>();
27+
let stdout = createSignal<string, void>();
28+
let stderr = createSignal<string, void>();
29+
let nodeproc = nodeSpawn(command, {
30+
shell: true,
31+
stdio: "pipe",
32+
});
33+
34+
// fail on an "error" event, but only until the process is successfully spawned.
35+
yield* spawn(function* () {
36+
yield* spawn(() =>
37+
action<void>((_, reject) => {
38+
nodeproc.on("error", reject);
39+
return () => nodeproc.off("error", reject);
40+
})
41+
);
42+
43+
yield* action((resolve) => {
44+
nodeproc.on("spawn", resolve);
45+
return () => nodeproc.off("spawn", resolve);
46+
});
47+
});
48+
49+
let onstdout = (chunk: unknown) => {
50+
stdout.send(String(chunk));
51+
};
52+
let onstderr = (chunk: unknown) => {
53+
stderr.send(String(chunk));
54+
};
55+
let onclose = (code: number, signal?: Signals) => {
56+
stdout.close();
57+
stderr.close();
58+
closed.resolve({ code, signal });
59+
};
60+
61+
try {
62+
nodeproc.stdout.on("data", onstdout);
63+
nodeproc.stderr.on("data", onstderr);
64+
nodeproc.on("close", onclose);
65+
66+
yield* provide({
67+
[Symbol.iterator]: closed.operation[Symbol.iterator],
68+
stdout,
69+
stderr,
70+
*send(signal) {
71+
nodeproc.kill(signal);
72+
},
73+
});
74+
} finally {
75+
nodeproc.kill("SIGINT");
76+
nodeproc.kill("SIGTERM");
77+
yield* closed.operation;
78+
stdout.close();
79+
stderr.close();
80+
nodeproc.stdout.off("data", onstdout);
81+
nodeproc.stderr.off("data", onstderr);
82+
nodeproc.off("close", onclose);
83+
}
84+
});
85+
}

watch/deno.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@effection-contrib/watch",
3+
"version": "0.1.0",
4+
"exports": {
5+
".": "./main.ts",
6+
"./lib": "./mod.ts"
7+
},
8+
"imports": {
9+
"effection": "npm:effection@^4.0.0-alpha.6",
10+
"ignore": "npm:ignore@^7.0.3",
11+
"@std/fs": "jsr:@std/fs@^1.0.11",
12+
"@std/path": "jsr:@std/path@^1.0.8",
13+
"chokidar": "npm:chokidar@^4.0.3",
14+
"zod": "npm:zod@^3.20.2",
15+
"zod-opts": "npm:zod-opts@0.1.8"
16+
},
17+
"lint": {
18+
"rules": {
19+
"exclude": ["prefer-const", "require-yield"]
20+
}
21+
},
22+
"tasks": {
23+
"compile": "deno compile --allow-env --allow-read --allow-run main.ts",
24+
"dev": "deno --allow-env --allow-read --allow-run main.ts deno task compile"
25+
}
26+
}

watch/deno.lock

Lines changed: 123 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

watch/main.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { each, exit, main, scoped, spawn } from "effection";
2+
import { watch } from "./watch.ts";
3+
import { parser } from "zod-opts";
4+
import { z } from "zod";
5+
import process from "node:process";
6+
import denoJson from "./deno.json" with { type: "json" };
7+
8+
const builtins = ["-h", "--help", "-V", "--version"];
9+
10+
await main(function* (argv) {
11+
let { args, rest } = extract(argv);
12+
parser()
13+
.name("watch")
14+
.description(
15+
"run a command, and restart it every time a source file in a directory changes",
16+
)
17+
.args([
18+
{
19+
name: "command",
20+
type: z.array(z.string()).optional(),
21+
},
22+
])
23+
.version(denoJson.version)
24+
.parse(args);
25+
26+
if (rest.length === 0) {
27+
yield* exit(5, "no command specified to watch");
28+
}
29+
30+
let command = rest.join(" ");
31+
32+
let watcher = watch({
33+
path: process.cwd(),
34+
cmd: command,
35+
});
36+
37+
for (let start of yield* each(watcher)) {
38+
process.stdout.write(`${command}\n`);
39+
yield* scoped(function* () {
40+
let { result } = start;
41+
if (result.ok) {
42+
let proc = result.value;
43+
yield* spawn(function* () {
44+
for (let chunk of yield* each(proc.stdout)) {
45+
process.stdout.write(chunk);
46+
yield* each.next();
47+
}
48+
});
49+
yield* spawn(function* () {
50+
for (let chunk of yield* each(proc.stderr)) {
51+
process.stderr.write(chunk);
52+
yield* each.next();
53+
}
54+
});
55+
} else {
56+
console.error(`failed to start: ${result.error}`);
57+
}
58+
yield* start.restarting;
59+
process.stdout.write(`--> restarting....\n`);
60+
yield* each.next();
61+
});
62+
}
63+
});
64+
65+
interface Extract {
66+
args: string[];
67+
rest: string[];
68+
}
69+
70+
function extract(argv: string[]): Extract {
71+
let args: string[] = [];
72+
let rest: string[] = argv.slice();
73+
74+
for (let arg = rest.shift(); arg; arg = rest.shift()) {
75+
if (builtins.includes(arg)) {
76+
args.push(arg);
77+
} else {
78+
rest.unshift(arg);
79+
break;
80+
}
81+
}
82+
return { args, rest };
83+
}

0 commit comments

Comments
 (0)