@mygo-plugins/watch 0.0.0-stage → 0.5.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MyGo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,39 @@
1
- # Temporary Holding Version
1
+ # @mygo-plugins/watch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Changes to files for [MyGo](https://github.com/egoist/mygo) apps' pages, in
4
+ the directories and files the app lets them watch, such as a project open
5
+ in an editor.
6
+
7
+ Use the plugin in Go with the directories pages may watch, each under a
8
+ name:
9
+
10
+ ```go
11
+ import "github.com/egoist/mygo/plugins/watch"
12
+
13
+ mygo.Use(watch.New(watch.Options{
14
+ Roots: map[string]watch.Root{
15
+ "project": {Path: projectDir, Recursive: true},
16
+ },
17
+ }))
18
+ ```
19
+
20
+ and watch one by its name in the frontend:
21
+
22
+ ```ts
23
+ import { watch } from "@mygo-plugins/watch";
24
+
25
+ const watcher = await watch("project", { recursive: true });
26
+ for await (const events of watcher) {
27
+ for (const event of events) console.log(event.op, event.path); // "write", "src/main.ts"
28
+ }
29
+ ```
30
+
31
+ Each batch has the events of `create`, `write`, `remove` and `rename` (with
32
+ `oldPath`) found once changes stopped for 50 ms (`debounceMs`), with paths
33
+ relative to what is watched, `path` in the root or the root itself. They say
34
+ what is different, not every step: treat them as hints to read the files
35
+ again. `watcher.close()`, breaking out of the loop, an
36
+ `AbortSignal` (`signal`) or the page navigating stop the watch.
37
+
38
+ [The documentation](https://github.com/egoist/mygo/blob/main/docs/plugins/watch.md)
39
+ covers the options of the Go side, errors and how each platform watches.
@@ -0,0 +1,51 @@
1
+ /** The kind of a {@link FileEvent}. */
2
+ export type FileOp = "create" | "write" | "remove" | "rename";
3
+ /**
4
+ * A change in a watched directory or file. Events say what is different
5
+ * once changes stop, not every step that made it so: treat them as hints
6
+ * to read the files again.
7
+ */
8
+ export interface FileEvent {
9
+ op: FileOp;
10
+ /** The path relative to what is watched, separated by slashes; "." is what is watched itself. */
11
+ path: string;
12
+ /** Where a rename moved from. */
13
+ oldPath?: string;
14
+ isDir: boolean;
15
+ }
16
+ /** Options of {@link watch}. */
17
+ export interface WatchOptions {
18
+ /** What to watch in the root, a directory or a file, relative to it and separated by slashes: the root itself by default. */
19
+ path?: string;
20
+ /** Watch the directories in the directory too, if the app allows it. */
21
+ recursive?: boolean;
22
+ /** How long changes must stop before they are reported: 50 ms when 0 or omitted, right away when negative. */
23
+ debounceMs?: number;
24
+ /** Stops the watch when aborted. */
25
+ signal?: AbortSignal;
26
+ }
27
+ /** Why a watch failed or stopped; see {@link WatchError}. */
28
+ export type WatchErrorCode = "denied" | "limit" | "not-found" | "unsupported" | "path-encoding" | "io";
29
+ /** A watch that failed to start or stopped, with the reason as its code. */
30
+ export declare class WatchError extends Error {
31
+ readonly code: WatchErrorCode;
32
+ constructor(code: WatchErrorCode, message: string);
33
+ }
34
+ /**
35
+ * A running watch: iterate it for the changes, a batch of events each time
36
+ * they stop. Go doesn't look again until the page has taken a batch, so
37
+ * nothing is lost while the page is busy.
38
+ */
39
+ export interface Watcher extends AsyncIterable<FileEvent[]> {
40
+ /** Settles once Go has stopped the watch, rejecting with the error that stopped it. */
41
+ readonly closed: Promise<void>;
42
+ /** Stops the watch; it resolves once Go has. */
43
+ close(): Promise<void>;
44
+ }
45
+ /**
46
+ * Watches the directory or file the app named `root`, or `path` in it, and
47
+ * resolves once everything is watched. What is already there is not
48
+ * reported. When it is removed, renamed or replaced, the last batch has a
49
+ * "remove" of ".", and the iteration ends.
50
+ */
51
+ export declare function watch(root: string, options?: WatchOptions): Promise<Watcher>;
package/dist/index.js ADDED
@@ -0,0 +1,109 @@
1
+ // src/index.ts
2
+ import { Channel, call } from "mygo-runtime";
3
+
4
+ class WatchError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "WatchError";
10
+ }
11
+ }
12
+ var codes = ["denied", "limit", "not-found", "unsupported", "path-encoding", "io"];
13
+ async function watch(root, options = {}) {
14
+ const debounceMs = options.debounceMs ?? 0;
15
+ if (!Number.isInteger(debounceMs) || debounceMs > 60000) {
16
+ throw new TypeError("debounceMs must be an integer up to 60000");
17
+ }
18
+ const path = options.path ?? ".";
19
+ if (typeof path !== "string")
20
+ throw new TypeError("path must be a string");
21
+ if (options.signal?.aborted)
22
+ throw options.signal.reason;
23
+ const parts = new Channel;
24
+ const stream = parts[Symbol.asyncIterator]();
25
+ let terminal;
26
+ let closing = false;
27
+ let finished = false;
28
+ let iterating = false;
29
+ const record = (error) => {
30
+ terminal ??= { error };
31
+ };
32
+ const abort = () => {
33
+ if (finished || closing)
34
+ return;
35
+ record(options.signal.reason);
36
+ closing = true;
37
+ parts.close();
38
+ };
39
+ options.signal?.addEventListener("abort", abort, { once: true });
40
+ const closed = call("plugin:watch.Watch", { root, path, recursive: options.recursive ?? false, debounceMs }, parts).then((outcome) => {
41
+ if (outcome?.code) {
42
+ record(codes.includes(outcome.code) ? new WatchError(outcome.code, outcome.message ?? "filesystem watch failed") : new Error("Invalid watch outcome"));
43
+ }
44
+ if (terminal)
45
+ throw terminal.error;
46
+ }, (err) => {
47
+ record(err);
48
+ throw terminal.error;
49
+ }).finally(() => {
50
+ finished = true;
51
+ options.signal?.removeEventListener("abort", abort);
52
+ });
53
+ closed.catch(() => {});
54
+ const close = async () => {
55
+ if (!closing && !finished) {
56
+ closing = true;
57
+ parts.close();
58
+ }
59
+ await closed.catch(() => {});
60
+ };
61
+ try {
62
+ const first = await stream.next();
63
+ if (first.done) {
64
+ await closed;
65
+ throw new Error("Watch ended before ready");
66
+ }
67
+ if (first.value.type !== "ready") {
68
+ parts.close();
69
+ await closed;
70
+ throw new Error("Invalid watch ready response");
71
+ }
72
+ if (terminal) {
73
+ await closed;
74
+ throw terminal.error;
75
+ }
76
+ } catch (err) {
77
+ parts.close();
78
+ options.signal?.removeEventListener("abort", abort);
79
+ throw err;
80
+ }
81
+ return {
82
+ closed,
83
+ close,
84
+ [Symbol.asyncIterator]() {
85
+ if (iterating)
86
+ throw new Error("A watch supports one iterator");
87
+ iterating = true;
88
+ return async function* () {
89
+ try {
90
+ while (!closing) {
91
+ const next = await stream.next();
92
+ if (next.done)
93
+ break;
94
+ if (next.value.events?.length && !closing)
95
+ yield next.value.events;
96
+ }
97
+ await closed;
98
+ } finally {
99
+ await close();
100
+ iterating = false;
101
+ }
102
+ }();
103
+ }
104
+ };
105
+ }
106
+ export {
107
+ WatchError,
108
+ watch
109
+ };
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@mygo-plugins/watch",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "Watches for changes to files in the directories a MyGo desktop app lets its pages watch",
5
+ "keywords": [
6
+ "mygo",
7
+ "desktop",
8
+ "webview",
9
+ "go",
10
+ "watch",
11
+ "filesystem"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/egoist/mygo.git",
16
+ "directory": "plugins/watch"
17
+ },
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "types": "./dist/index.d.ts",
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "bun run ../build.ts",
34
+ "test": "bun test"
35
+ },
36
+ "peerDependencies": {
37
+ "mygo-runtime": "^0.5.0"
38
+ },
39
+ "devDependencies": {
40
+ "mygo-runtime": "^0.5.0"
41
+ }
42
+ }