pursr 0.5.0 → 0.7.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/src/watch.js CHANGED
@@ -1,134 +1,134 @@
1
- // pursor - watch mode.
2
- //
3
- // Re-runs a shoot/sweep whenever watched files change.
4
-
5
- import { watch as fsWatch, existsSync } from "node:fs";
6
- import { resolve, join, dirname, relative } from "node:path";
7
- import { runShootWithSidecar } from "./shoot.js";
8
- import { runSweep } from "./sweep.js";
9
- import { nowIso } from "./util.js";
10
-
11
- function normalizeGlobs(globs) {
12
- if (!globs) return null;
13
- const arr = Array.isArray(globs) ? globs : [globs];
14
- return arr.filter(Boolean);
15
- }
16
-
17
- const BC = String.fromCharCode(92);
18
- const ESC_RX = /[.+^$X()|YZ\\]/g;
19
- function escapeForRegex(s) {
20
- return s.replace(ESC_RX, function (m) {
21
- if (m === "X") return BC + "$";
22
- if (m === "Y") return BC + "{";
23
- if (m === "Z") return BC + "}";
24
- return BC + m;
25
- });
26
- }
27
-
28
- function matchGlob(path, pattern) {
29
- const p = String(path).split(BC).join("/");
30
- const pat = String(pattern);
31
- let re = "^";
32
- for (let i = 0; i < pat.length; i++) {
33
- const c = pat[i];
34
- if (c === BC) {
35
- const next = pat[i + 1];
36
- if (next === undefined) { re += BC + BC; continue; }
37
- re += escapeForRegex(next);
38
- i++;
39
- } else if (c === "*") {
40
- if (pat[i + 1] === "*") { re += ".*"; i++; }
41
- else { re += "[^/]*"; }
42
- } else if (c === "?") {
43
- re += "[^/]";
44
- } else {
45
- re += escapeForRegex(c);
46
- }
47
- }
48
- re += "$";
49
- return new RegExp(re).test(p);
50
- }
51
-
52
- function shouldFire(path, globs) {
53
- if (!globs || globs.length === 0) return true;
54
- return globs.some((g) => matchGlob(path, g));
55
- }
56
-
57
- function debounce(fn, ms) {
58
- let t = null;
59
- return (...args) => {
60
- if (t) clearTimeout(t);
61
- t = setTimeout(() => { t = null; fn(...args); }, ms);
62
- };
63
- }
64
-
65
- export async function startWatch(opts) {
66
- if (!opts.url && !opts.plan) throw new Error("startWatch: requires url or plan");
67
- const globs = normalizeGlobs(opts.on);
68
- const debounceMs = opts.debounceMs ?? 300;
69
- const verbose = !!opts.verbose;
70
-
71
- let fireCount = 0;
72
- let runningPromise = Promise.resolve();
73
- let closed = false;
74
-
75
- const runOne = async (event) => {
76
- if (closed) return;
77
- fireCount++;
78
- try {
79
- let capture = null;
80
- if (opts.plan) {
81
- capture = await runSweep(opts.plan, opts.outDir);
82
- } else {
83
- capture = await runShootWithSidecar({ url: opts.url, out: opts.out, flags: opts.flags || {} });
84
- }
85
- if (typeof opts.onChange === "function") {
86
- try { await opts.onChange({ ...event, capture }); } catch (e) {
87
- if (verbose) console.error("[pursr watch] onChange error:", e.message);
88
- }
89
- }
90
- } catch (e) {
91
- if (verbose) console.error("[pursr watch] capture error:", e.message);
92
- }
93
- };
94
-
95
- const debouncedRun = debounce((event) => { runningPromise = runningPromise.then(() => runOne(event)); }, debounceMs);
96
-
97
- const targets = globs && globs.length > 0
98
- ? globs.map((g) => {
99
- const lit = g.split(/[*?]/)[0];
100
- return resolve(lit || ".");
101
- })
102
- : [resolve(process.cwd())];
103
-
104
- const watchers = [];
105
- for (const target of targets) {
106
- if (!existsSync(target)) continue;
107
- try {
108
- const w = fsWatch(target, { recursive: true }, (eventType, filename) => {
109
- if (!filename) return;
110
- const full = join(target, filename);
111
- const rel = relative(process.cwd(), full).split(BC).join("/");
112
- if (!shouldFire(rel, globs)) return;
113
- debouncedRun({ type: eventType, path: full, ts: nowIso() });
114
- });
115
- w.on("error", (e) => { if (verbose) console.error("[pursr watch] watcher error:", e.message); });
116
- watchers.push(w);
117
- } catch (e) {
118
- if (verbose) console.error("[pursr watch] cannot watch", target, e.message);
119
- }
120
- }
121
-
122
- runningPromise = runningPromise.then(() => runOne({ type: "init", path: null, ts: nowIso() }));
123
-
124
- return {
125
- close: async () => {
126
- closed = true;
127
- for (const w of watchers) { try { w.close(); } catch {} }
128
- await runningPromise.catch(() => {});
129
- },
130
- fires: () => fireCount,
131
- };
132
- }
133
-
134
- export { matchGlob, shouldFire };
1
+ // pursr - watch mode.
2
+ //
3
+ // Re-runs a shoot/sweep whenever watched files change.
4
+
5
+ import { watch as fsWatch, existsSync } from "node:fs";
6
+ import { resolve, join, dirname, relative } from "node:path";
7
+ import { runShootWithSidecar } from "./shoot.js";
8
+ import { runSweep } from "./sweep.js";
9
+ import { nowIso } from "./util.js";
10
+
11
+ function normalizeGlobs(globs) {
12
+ if (!globs) return null;
13
+ const arr = Array.isArray(globs) ? globs : [globs];
14
+ return arr.filter(Boolean);
15
+ }
16
+
17
+ const BC = String.fromCharCode(92);
18
+ const ESC_RX = /[.+^$X()|YZ\\]/g;
19
+ function escapeForRegex(s) {
20
+ return s.replace(ESC_RX, function (m) {
21
+ if (m === "X") return BC + "$";
22
+ if (m === "Y") return BC + "{";
23
+ if (m === "Z") return BC + "}";
24
+ return BC + m;
25
+ });
26
+ }
27
+
28
+ function matchGlob(path, pattern) {
29
+ const p = String(path).split(BC).join("/");
30
+ const pat = String(pattern);
31
+ let re = "^";
32
+ for (let i = 0; i < pat.length; i++) {
33
+ const c = pat[i];
34
+ if (c === BC) {
35
+ const next = pat[i + 1];
36
+ if (next === undefined) { re += BC + BC; continue; }
37
+ re += escapeForRegex(next);
38
+ i++;
39
+ } else if (c === "*") {
40
+ if (pat[i + 1] === "*") { re += ".*"; i++; }
41
+ else { re += "[^/]*"; }
42
+ } else if (c === "?") {
43
+ re += "[^/]";
44
+ } else {
45
+ re += escapeForRegex(c);
46
+ }
47
+ }
48
+ re += "$";
49
+ return new RegExp(re).test(p);
50
+ }
51
+
52
+ function shouldFire(path, globs) {
53
+ if (!globs || globs.length === 0) return true;
54
+ return globs.some((g) => matchGlob(path, g));
55
+ }
56
+
57
+ function debounce(fn, ms) {
58
+ let t = null;
59
+ return (...args) => {
60
+ if (t) clearTimeout(t);
61
+ t = setTimeout(() => { t = null; fn(...args); }, ms);
62
+ };
63
+ }
64
+
65
+ export async function startWatch(opts) {
66
+ if (!opts.url && !opts.plan) throw new Error("startWatch: requires url or plan");
67
+ const globs = normalizeGlobs(opts.on);
68
+ const debounceMs = opts.debounceMs ?? 300;
69
+ const verbose = !!opts.verbose;
70
+
71
+ let fireCount = 0;
72
+ let runningPromise = Promise.resolve();
73
+ let closed = false;
74
+
75
+ const runOne = async (event) => {
76
+ if (closed) return;
77
+ fireCount++;
78
+ try {
79
+ let capture = null;
80
+ if (opts.plan) {
81
+ capture = await runSweep(opts.plan, opts.outDir);
82
+ } else {
83
+ capture = await runShootWithSidecar({ url: opts.url, out: opts.out, flags: opts.flags || {} });
84
+ }
85
+ if (typeof opts.onChange === "function") {
86
+ try { await opts.onChange({ ...event, capture }); } catch (e) {
87
+ if (verbose) console.error("[pursr watch] onChange error:", e.message);
88
+ }
89
+ }
90
+ } catch (e) {
91
+ if (verbose) console.error("[pursr watch] capture error:", e.message);
92
+ }
93
+ };
94
+
95
+ const debouncedRun = debounce((event) => { runningPromise = runningPromise.then(() => runOne(event)); }, debounceMs);
96
+
97
+ const targets = globs && globs.length > 0
98
+ ? globs.map((g) => {
99
+ const lit = g.split(/[*?]/)[0];
100
+ return resolve(lit || ".");
101
+ })
102
+ : [resolve(process.cwd())];
103
+
104
+ const watchers = [];
105
+ for (const target of targets) {
106
+ if (!existsSync(target)) continue;
107
+ try {
108
+ const w = fsWatch(target, { recursive: true }, (eventType, filename) => {
109
+ if (!filename) return;
110
+ const full = join(target, filename);
111
+ const rel = relative(process.cwd(), full).split(BC).join("/");
112
+ if (!shouldFire(rel, globs)) return;
113
+ debouncedRun({ type: eventType, path: full, ts: nowIso() });
114
+ });
115
+ w.on("error", (e) => { if (verbose) console.error("[pursr watch] watcher error:", e.message); });
116
+ watchers.push(w);
117
+ } catch (e) {
118
+ if (verbose) console.error("[pursr watch] cannot watch", target, e.message);
119
+ }
120
+ }
121
+
122
+ runningPromise = runningPromise.then(() => runOne({ type: "init", path: null, ts: nowIso() }));
123
+
124
+ return {
125
+ close: async () => {
126
+ closed = true;
127
+ for (const w of watchers) { try { w.close(); } catch {} }
128
+ await runningPromise.catch(() => {});
129
+ },
130
+ fires: () => fireCount,
131
+ };
132
+ }
133
+
134
+ export { matchGlob, shouldFire };