react-hooks-global-states-debug 16.0.4 → 16.0.5
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/cli.mjs +706 -0
- package/debug.js +45 -32
- package/index.d.ts +1 -1
- package/package.json +22 -3
package/cli.mjs
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/agent/protocol.ts
|
|
4
|
+
var AGENT_DEFAULT_PORT = 7787;
|
|
5
|
+
var AGENT_CLOSE_SUPERSEDED = 4001;
|
|
6
|
+
var ALL_STORES = "*";
|
|
7
|
+
|
|
8
|
+
// src/cli/args.ts
|
|
9
|
+
var DEFAULT_TIMEOUT_SECONDS = 10;
|
|
10
|
+
var HELP = `rgsh: see what happens inside your stores from a terminal, and drive them
|
|
11
|
+
|
|
12
|
+
WHAT IT DOES
|
|
13
|
+
Connects to the React Global State Hooks DevTools panel and streams one block per store action:
|
|
14
|
+
the store, the action and its input, the state changes it caused, the result or error, and the
|
|
15
|
+
duration. Other commands read a store, run an action, or change its state.
|
|
16
|
+
|
|
17
|
+
BEFORE YOU START
|
|
18
|
+
1. The app must import the debug package (import 'react-hooks-global-states/debug').
|
|
19
|
+
2. Chrome has the DevTools extension, with DevTools open on the app tab and the
|
|
20
|
+
react-global-state-hooks panel opened once. rgsh cannot start without it.
|
|
21
|
+
3. Same port on both sides: rgsh listens on 7787 by default; change it with --port and in the
|
|
22
|
+
panel under gear > Terminal connection.
|
|
23
|
+
|
|
24
|
+
DISCOVER
|
|
25
|
+
rgsh --list every store: name or creation location, state preview, actions
|
|
26
|
+
rgsh pick stores with the arrow keys (a list when there is no terminal)
|
|
27
|
+
rgsh state <store> [path] current state and metadata. path: todos[0].done, user.name
|
|
28
|
+
A store is a name, or the creation location of an unnamed store
|
|
29
|
+
(ShoppingCart.tsx:23), as printed by --list.
|
|
30
|
+
|
|
31
|
+
WATCH
|
|
32
|
+
rgsh --store todos one store
|
|
33
|
+
rgsh --store todos,auth several (comma separated, or repeat --store)
|
|
34
|
+
rgsh --store "*" every store: DevTools does more work and the stream is much larger
|
|
35
|
+
|
|
36
|
+
CHANGE
|
|
37
|
+
rgsh action <store> <name> [args...] run an action: rgsh action todos add "Write the docs"
|
|
38
|
+
rgsh patch <store> <json> objects merge into the state, anything else replaces it
|
|
39
|
+
rgsh patch todos '{"filter":"done"}' rgsh patch counter 5
|
|
40
|
+
rgsh set <store> <json> replace the whole state (like the State tab editor)
|
|
41
|
+
Each command prints the resulting block (state changes, result or error) and exits.
|
|
42
|
+
|
|
43
|
+
VALUES
|
|
44
|
+
Arguments and states are JSON: 5, true, null, {"a":1}, ["x"], "text". A plain word is text.
|
|
45
|
+
A value that starts with { [ or " must be valid JSON. Never code: nothing is evaluated.
|
|
46
|
+
Numbers and booleans that should be text need quotes: '"5"'.
|
|
47
|
+
|
|
48
|
+
OPTIONS
|
|
49
|
+
--store, -s <names> stores to watch
|
|
50
|
+
--list, -l list stores and exit
|
|
51
|
+
--port <number> port to listen on (default ${AGENT_DEFAULT_PORT}); must match the panel
|
|
52
|
+
--timeout <seconds> how long action/patch/set wait for the result (default ${DEFAULT_TIMEOUT_SECONDS})
|
|
53
|
+
--help, -h
|
|
54
|
+
|
|
55
|
+
READING THE OUTPUT
|
|
56
|
+
12:03:41.221 [todos] add("Write the docs") time, store, action(input)
|
|
57
|
+
state: state (1/2), (2/2)... when the action set state several times
|
|
58
|
+
todos[2]: undefined \u2192 {"id":3} path: before \u2192 after; undefined = did not exist / removed
|
|
59
|
+
result: {"ok":true} omitted when the action returned nothing
|
|
60
|
+
error: Error: boom instead of result when the action threw
|
|
61
|
+
duration: 6ms
|
|
62
|
+
[name #2] means several live instances share that store. "setState" is a direct state change.
|
|
63
|
+
"store created" / "store removed": the store appeared or disappeared (mount, unmount, reload).
|
|
64
|
+
Events print when an action finishes, so a slow async action appears after it settles.
|
|
65
|
+
|
|
66
|
+
EXIT CODES
|
|
67
|
+
0 done 1 failed (unknown store or action, not allowed, action threw) 2 no --store given, no terminal
|
|
68
|
+
3 sent, but the page reported nothing within --timeout
|
|
69
|
+
|
|
70
|
+
GOOD TO KNOW
|
|
71
|
+
- Unnamed stores are identified by where they were created; name your stores in minified builds.
|
|
72
|
+
- Metadata shown by "state" is what the page announced when the store was created. Changes made
|
|
73
|
+
later with setMetadata are not reported by the debug package.
|
|
74
|
+
- Values that cannot be serialized (functions, DOM nodes) appear as {"__non_serializable__": ...}
|
|
75
|
+
and are kept as they are when you patch or set.
|
|
76
|
+
- Large values are cut with an explicit marker such as \u2026(+120 chars) or \u2026(+8 items).
|
|
77
|
+
- Only one store at a time for action, set, patch and state. A store with several live
|
|
78
|
+
instances is refused.
|
|
79
|
+
- Ctrl+C stops watching; DevTools goes back to normal and the terminal work stops.`;
|
|
80
|
+
var UsageError = class extends Error {
|
|
81
|
+
};
|
|
82
|
+
var parseValue = (text) => {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(text);
|
|
85
|
+
} catch {
|
|
86
|
+
if (/^\s*[{["]/.test(text)) throw new UsageError(`Not valid JSON: ${text}`);
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var parseCommand = (positionals) => {
|
|
91
|
+
const [name, store, ...rest] = positionals;
|
|
92
|
+
if (name === void 0) return null;
|
|
93
|
+
if (name === "action") {
|
|
94
|
+
const [action, ...args] = rest;
|
|
95
|
+
if (!store || !action) throw new UsageError("Usage: rgsh action <store> <action> [args...]");
|
|
96
|
+
return { kind: "action", store, action, args: args.map(parseValue) };
|
|
97
|
+
}
|
|
98
|
+
if (name === "set" || name === "patch") {
|
|
99
|
+
if (!store || rest.length !== 1) throw new UsageError(`Usage: rgsh ${name} <store> <json>`);
|
|
100
|
+
const value = parseValue(rest[0]);
|
|
101
|
+
return name === "set" ? { kind: "set", store, state: value } : { kind: "patch", store, patch: value };
|
|
102
|
+
}
|
|
103
|
+
if (name === "state") {
|
|
104
|
+
if (!store || rest.length > 1) throw new UsageError("Usage: rgsh state <store> [path]");
|
|
105
|
+
return { kind: "state", store, path: rest[0] ?? "" };
|
|
106
|
+
}
|
|
107
|
+
throw new UsageError(`Unknown command "${name}"`);
|
|
108
|
+
};
|
|
109
|
+
var parseArgs = (argv) => {
|
|
110
|
+
const options = {
|
|
111
|
+
targets: [],
|
|
112
|
+
command: null,
|
|
113
|
+
timeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
|
|
114
|
+
list: false,
|
|
115
|
+
help: false,
|
|
116
|
+
port: AGENT_DEFAULT_PORT
|
|
117
|
+
};
|
|
118
|
+
const positionals = [];
|
|
119
|
+
const takeValue = (flag, inline, index) => {
|
|
120
|
+
if (inline !== void 0) return [inline, index];
|
|
121
|
+
const value = argv[index + 1];
|
|
122
|
+
if (value === void 0 || value.startsWith("-") && value !== ALL_STORES) {
|
|
123
|
+
throw new UsageError(`${flag} needs a value`);
|
|
124
|
+
}
|
|
125
|
+
return [value, index + 1];
|
|
126
|
+
};
|
|
127
|
+
for (let index = 0; index < argv.length; index++) {
|
|
128
|
+
const [flag, inline] = argv[index].split(/=(.*)/s, 2);
|
|
129
|
+
if (flag === "--help" || flag === "-h") options.help = true;
|
|
130
|
+
else if (flag === "--list" || flag === "-l") options.list = true;
|
|
131
|
+
else if (flag === "--store" || flag === "-s") {
|
|
132
|
+
const [value, next] = takeValue(flag, inline, index);
|
|
133
|
+
index = next;
|
|
134
|
+
options.targets.push(...value.split(",").map((target) => target.trim()).filter(Boolean));
|
|
135
|
+
} else if (flag === "--timeout") {
|
|
136
|
+
const [value, next] = takeValue(flag, inline, index);
|
|
137
|
+
index = next;
|
|
138
|
+
options.timeoutSeconds = Number(value);
|
|
139
|
+
if (!(options.timeoutSeconds > 0)) throw new UsageError(`--timeout must be a positive number, got "${value}"`);
|
|
140
|
+
} else if (flag === "--port") {
|
|
141
|
+
const [value, next] = takeValue(flag, inline, index);
|
|
142
|
+
index = next;
|
|
143
|
+
options.port = Number(value);
|
|
144
|
+
if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) {
|
|
145
|
+
throw new UsageError(`--port must be a number between 1 and 65535, got "${value}"`);
|
|
146
|
+
}
|
|
147
|
+
} else if (argv[index].startsWith("-") && argv[index] !== ALL_STORES && !/^-\d/.test(argv[index])) {
|
|
148
|
+
throw new UsageError(`Unknown option "${argv[index]}"`);
|
|
149
|
+
} else {
|
|
150
|
+
positionals.push(argv[index]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
options.command = parseCommand(positionals);
|
|
154
|
+
if (options.command && options.targets.length) {
|
|
155
|
+
throw new UsageError("--store does not combine with state/action/set/patch: name the store after the command");
|
|
156
|
+
}
|
|
157
|
+
return options;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/cli/format.ts
|
|
161
|
+
var INLINE_MAX = 100;
|
|
162
|
+
var pad = (value, size = 2) => String(value).padStart(size, "0");
|
|
163
|
+
var formatTime = (at) => {
|
|
164
|
+
const date = new Date(at);
|
|
165
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
|
166
|
+
};
|
|
167
|
+
var formatStoreRef = ({ label, instance }) => `[${label}${instance ? ` #${instance}` : ""}]`;
|
|
168
|
+
var formatValue = (value, indent) => {
|
|
169
|
+
if (value === void 0) return "undefined";
|
|
170
|
+
const inline = JSON.stringify(value);
|
|
171
|
+
if (inline.length <= INLINE_MAX) return inline;
|
|
172
|
+
return JSON.stringify(value, null, 2).replace(/\n/g, `
|
|
173
|
+
${indent}`);
|
|
174
|
+
};
|
|
175
|
+
var formatChange = (change, indent) => {
|
|
176
|
+
const { path, before, after } = change;
|
|
177
|
+
if (!("before" in change) && !("after" in change)) return path;
|
|
178
|
+
return `${path || "(state)"}: ${formatValue(before, indent)} \u2192 ${formatValue(after, indent)}`;
|
|
179
|
+
};
|
|
180
|
+
var formatSteps = (event) => {
|
|
181
|
+
const { steps, stateCalls } = event;
|
|
182
|
+
if (!stateCalls) return [];
|
|
183
|
+
if (!steps.length) return [" state: no change"];
|
|
184
|
+
return steps.flatMap((changes, index) => {
|
|
185
|
+
const title = steps.length > 1 ? ` state (${index + 1}/${steps.length}):` : " state:";
|
|
186
|
+
return [title, ...changes.map((change) => ` ${formatChange(change, " ")}`)];
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
var formatAction = (event) => {
|
|
190
|
+
const { store, action, input, result, durationMs } = event;
|
|
191
|
+
const call = action === "setState" ? "setState" : `${action}(${(input ?? []).map((arg) => JSON.stringify(arg)).join(", ")})`;
|
|
192
|
+
const lines = [`${formatTime(event.at)} ${formatStoreRef(store)} ${call}`, ...formatSteps(event)];
|
|
193
|
+
if (result && "error" in result) lines.push(` error: ${result.error}`);
|
|
194
|
+
else if (result && result.value !== void 0) lines.push(` result: ${formatValue(result.value, " ")}`);
|
|
195
|
+
if (durationMs !== void 0) lines.push(` duration: ${durationMs}ms`);
|
|
196
|
+
return lines;
|
|
197
|
+
};
|
|
198
|
+
var formatEvent = (event) => {
|
|
199
|
+
if (event.kind === "action") return formatAction(event).join("\n");
|
|
200
|
+
const what = event.kind === "store-created" ? "store created" : "store removed";
|
|
201
|
+
return `${formatTime(event.at)} ${formatStoreRef(event.store)} ${what}`;
|
|
202
|
+
};
|
|
203
|
+
var storeTitle = (store) => {
|
|
204
|
+
if (store.name) return store.instances > 1 ? `${store.name} (${store.instances} instances)` : store.name;
|
|
205
|
+
return `unnamed \u2014 ${store.location ?? "unknown location"}`;
|
|
206
|
+
};
|
|
207
|
+
var storeLabel = (store) => store.name ?? `unnamed ${store.location ?? ""}`.trim();
|
|
208
|
+
var oneLine = (text, max = 120) => {
|
|
209
|
+
const flat = text.replace(/\s*\n\s*/g, " ");
|
|
210
|
+
return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
|
|
211
|
+
};
|
|
212
|
+
var formatStoreList = (stores) => {
|
|
213
|
+
if (!stores.length) return "No stores known by DevTools yet. Load the app with the debug package imported.";
|
|
214
|
+
return stores.map(
|
|
215
|
+
(store) => [
|
|
216
|
+
storeTitle(store),
|
|
217
|
+
...store.name && store.location ? [` at ${store.location}`] : [],
|
|
218
|
+
` state: ${oneLine(store.preview)}`,
|
|
219
|
+
...store.actions.length ? [` actions: ${store.actions.join(", ")}`] : []
|
|
220
|
+
].join("\n")
|
|
221
|
+
).join("\n\n");
|
|
222
|
+
};
|
|
223
|
+
var formatState = (reply) => {
|
|
224
|
+
const pretty = (value) => value === void 0 ? "undefined" : JSON.stringify(value, null, 2);
|
|
225
|
+
return [
|
|
226
|
+
`${formatStoreRef(reply.store)} state${reply.path ? ` at ${reply.path}` : ""}:`,
|
|
227
|
+
pretty(reply.state),
|
|
228
|
+
"",
|
|
229
|
+
"metadata (as announced when the store was created; later setMetadata calls are not reported):",
|
|
230
|
+
pretty(reply.metadata)
|
|
231
|
+
].join("\n");
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/cli/resolve.ts
|
|
235
|
+
var MAX_CANDIDATES = 8;
|
|
236
|
+
var editDistance = (a, b) => {
|
|
237
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
238
|
+
for (let i = 1; i <= a.length; i++) {
|
|
239
|
+
const current = [i];
|
|
240
|
+
for (let j = 1; j <= b.length; j++) {
|
|
241
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
242
|
+
}
|
|
243
|
+
previous = current;
|
|
244
|
+
}
|
|
245
|
+
return previous[b.length];
|
|
246
|
+
};
|
|
247
|
+
var findCandidates = (target, stores) => {
|
|
248
|
+
const needle = target.toLowerCase();
|
|
249
|
+
const labels = stores.map(storeLabel);
|
|
250
|
+
const close = labels.filter((label) => {
|
|
251
|
+
const candidate = label.toLowerCase();
|
|
252
|
+
return candidate.includes(needle) || needle.includes(candidate) || editDistance(candidate, needle) <= 2;
|
|
253
|
+
});
|
|
254
|
+
return (close.length ? close : labels).slice(0, MAX_CANDIDATES);
|
|
255
|
+
};
|
|
256
|
+
var resolveTargets = (targets, stores) => {
|
|
257
|
+
const selectors = /* @__PURE__ */ new Set();
|
|
258
|
+
const unknown = [];
|
|
259
|
+
for (const target of targets) {
|
|
260
|
+
const byName = stores.filter((store) => store.name === target);
|
|
261
|
+
const matches = byName.length ? byName : stores.filter((store) => store.location?.endsWith(target));
|
|
262
|
+
if (!matches.length) {
|
|
263
|
+
unknown.push({ target, candidates: findCandidates(target, stores) });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
for (const store of matches) selectors.add(store.selector);
|
|
267
|
+
}
|
|
268
|
+
return { selectors: [...selectors], unknown };
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/cli/server.ts
|
|
272
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
273
|
+
var PanelSession = class {
|
|
274
|
+
constructor(socket, hello) {
|
|
275
|
+
this.socket = socket;
|
|
276
|
+
this.hello = hello;
|
|
277
|
+
this.replyWaiters = /* @__PURE__ */ new Set();
|
|
278
|
+
this.eventListeners = [];
|
|
279
|
+
this.closeListeners = [];
|
|
280
|
+
socket.on("message", (data) => this.receive(data.toString()));
|
|
281
|
+
socket.on("close", () => {
|
|
282
|
+
for (const listener of this.closeListeners) listener();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
receive(text) {
|
|
286
|
+
let message;
|
|
287
|
+
try {
|
|
288
|
+
message = JSON.parse(text);
|
|
289
|
+
} catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (message.type === "EVENT") {
|
|
293
|
+
for (const listener of this.eventListeners) listener(message.event);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for (const waiter of [...this.replyWaiters]) waiter(message);
|
|
297
|
+
}
|
|
298
|
+
onEvent(listener) {
|
|
299
|
+
this.eventListeners.push(listener);
|
|
300
|
+
}
|
|
301
|
+
onClose(listener) {
|
|
302
|
+
this.closeListeners.push(listener);
|
|
303
|
+
}
|
|
304
|
+
send(message) {
|
|
305
|
+
this.socket.send(JSON.stringify(message));
|
|
306
|
+
}
|
|
307
|
+
/** Sends a request and resolves with the first reply of one of the expected types. */
|
|
308
|
+
request(message, expected) {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const finish = () => {
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
this.replyWaiters.delete(waiter);
|
|
313
|
+
this.closeListeners = this.closeListeners.filter((listener) => listener !== onClose);
|
|
314
|
+
};
|
|
315
|
+
const waiter = (reply) => {
|
|
316
|
+
if (!expected.includes(reply.type)) return;
|
|
317
|
+
finish();
|
|
318
|
+
resolve(reply);
|
|
319
|
+
};
|
|
320
|
+
const onClose = () => {
|
|
321
|
+
finish();
|
|
322
|
+
reject(new Error("DevTools panel disconnected"));
|
|
323
|
+
};
|
|
324
|
+
const timer = setTimeout(() => {
|
|
325
|
+
finish();
|
|
326
|
+
reject(new Error(`DevTools did not answer ${message.type} in ${REQUEST_TIMEOUT_MS / 1e3}s`));
|
|
327
|
+
}, REQUEST_TIMEOUT_MS);
|
|
328
|
+
this.replyWaiters.add(waiter);
|
|
329
|
+
this.closeListeners.push(onClose);
|
|
330
|
+
this.send(message);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var isAllowedOrigin = (origin) => origin === void 0 || origin.startsWith("chrome-extension://");
|
|
335
|
+
var MissingWsError = class extends Error {
|
|
336
|
+
constructor() {
|
|
337
|
+
super(
|
|
338
|
+
[
|
|
339
|
+
"rgsh needs the 'ws' package to talk to the DevTools extension, and it is not installed in this project.",
|
|
340
|
+
"Install it as a dev dependency:",
|
|
341
|
+
" yarn add -D ws",
|
|
342
|
+
" npm install -D ws",
|
|
343
|
+
" pnpm add -D ws"
|
|
344
|
+
].join("\n")
|
|
345
|
+
);
|
|
346
|
+
this.name = "MissingWsError";
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var loadWebSocketServer = async () => {
|
|
350
|
+
try {
|
|
351
|
+
const ws = await import("ws");
|
|
352
|
+
return ws.WebSocketServer ?? ws.default.Server;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error.code === "ERR_MODULE_NOT_FOUND") throw new MissingWsError();
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var startServer = async (port) => {
|
|
359
|
+
const WebSocketServer = await loadWebSocketServer();
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
361
|
+
const panelListeners = [];
|
|
362
|
+
let current = null;
|
|
363
|
+
const wss = new WebSocketServer({
|
|
364
|
+
host: "127.0.0.1",
|
|
365
|
+
port,
|
|
366
|
+
verifyClient: ({ origin }) => isAllowedOrigin(origin)
|
|
367
|
+
});
|
|
368
|
+
wss.once("error", reject);
|
|
369
|
+
wss.on("connection", (socket) => {
|
|
370
|
+
socket.once("message", (data) => {
|
|
371
|
+
let hello;
|
|
372
|
+
try {
|
|
373
|
+
hello = JSON.parse(data.toString());
|
|
374
|
+
} catch {
|
|
375
|
+
return socket.close();
|
|
376
|
+
}
|
|
377
|
+
if (hello.type !== "HELLO") return socket.close();
|
|
378
|
+
current?.close(AGENT_CLOSE_SUPERSEDED, "a newer DevTools panel connected");
|
|
379
|
+
current = socket;
|
|
380
|
+
socket.on("close", () => {
|
|
381
|
+
if (current === socket) current = null;
|
|
382
|
+
});
|
|
383
|
+
const panel = new PanelSession(socket, hello);
|
|
384
|
+
for (const listener of panelListeners) listener(panel);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
wss.once("listening", () => {
|
|
388
|
+
wss.off("error", reject);
|
|
389
|
+
wss.on("error", () => void 0);
|
|
390
|
+
resolve({
|
|
391
|
+
port: wss.address().port,
|
|
392
|
+
onPanel: (listener) => panelListeners.push(listener),
|
|
393
|
+
close: () => new Promise((done) => {
|
|
394
|
+
for (const client of wss.clients) client.close();
|
|
395
|
+
wss.close(() => done());
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/cli/run.ts
|
|
403
|
+
var EXIT = { ok: 0, failed: 1, needsTarget: 2, noOutcome: 3 };
|
|
404
|
+
var ALL_WARNING = 'warning: --store "*" listens to every store. DevTools does extra processing for every state change and this stream gets much larger. Naming the stores you need (--store todos,auth) is more efficient.';
|
|
405
|
+
var RESUBSCRIBE_ATTEMPTS = 5;
|
|
406
|
+
var RESUBSCRIBE_DELAY_MS = 1e3;
|
|
407
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
408
|
+
var describePanel = ({ tabId, page }) => [tabId !== null ? `tab ${tabId}` : null, page].filter(Boolean).join(" ");
|
|
409
|
+
var run = async (options, io, signal) => {
|
|
410
|
+
let server;
|
|
411
|
+
try {
|
|
412
|
+
server = await startServer(options.port);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const { code } = error;
|
|
415
|
+
io.err(
|
|
416
|
+
error instanceof MissingWsError ? error.message : code === "EADDRINUSE" ? `Port ${options.port} is already in use. Is another rgsh running? Stop it or pass --port.` : `Could not start the listener on port ${options.port}: ${error.message}`
|
|
417
|
+
);
|
|
418
|
+
return EXIT.failed;
|
|
419
|
+
}
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
let current = null;
|
|
422
|
+
let subscribed = null;
|
|
423
|
+
let finished = false;
|
|
424
|
+
let commandStarted = false;
|
|
425
|
+
const finish = async (code) => {
|
|
426
|
+
if (finished) return;
|
|
427
|
+
finished = true;
|
|
428
|
+
await server.close();
|
|
429
|
+
resolve(code);
|
|
430
|
+
};
|
|
431
|
+
signal.addEventListener("abort", () => void finish(EXIT.ok), { once: true });
|
|
432
|
+
const subscribe = async (panel, selectors) => {
|
|
433
|
+
const reply = await panel.request({ type: "SUBSCRIBE", selectors }, ["SUBSCRIBED", "REJECTED"]);
|
|
434
|
+
if (reply.type === "REJECTED") return reply;
|
|
435
|
+
subscribed = selectors;
|
|
436
|
+
const names = reply.matched.length ? reply.matched.map(formatStoreRef).join(" ") : "(no store yet)";
|
|
437
|
+
io.err(`Listening to ${reply.all ? "all stores" : names}. Press Ctrl+C to stop.`);
|
|
438
|
+
return null;
|
|
439
|
+
};
|
|
440
|
+
const runCommand = async (panel, command) => {
|
|
441
|
+
const { stores } = await panel.request({ type: "GET_STORES" }, ["AVAILABLE_STORES"]);
|
|
442
|
+
const resolved = resolveTargets([command.store], stores);
|
|
443
|
+
if (resolved.unknown.length) {
|
|
444
|
+
const [{ target, candidates }] = resolved.unknown;
|
|
445
|
+
io.err(`Store "${target}" was not found. Available: ${candidates.join(", ") || "(none)"}`);
|
|
446
|
+
return finish(EXIT.failed);
|
|
447
|
+
}
|
|
448
|
+
const [selector] = resolved.selectors;
|
|
449
|
+
if (resolved.selectors.length !== 1) {
|
|
450
|
+
io.err(`"${command.store}" matches ${resolved.selectors.length} stores. Name exactly one.`);
|
|
451
|
+
return finish(EXIT.failed);
|
|
452
|
+
}
|
|
453
|
+
if (command.kind === "state") {
|
|
454
|
+
const reply2 = await panel.request({ type: "GET_STATE", selector, path: command.path }, ["STATE", "REQUEST_REJECTED"]);
|
|
455
|
+
if (reply2.type === "REQUEST_REJECTED") {
|
|
456
|
+
io.err(reply2.reason);
|
|
457
|
+
return finish(EXIT.failed);
|
|
458
|
+
}
|
|
459
|
+
if (!reply2.found) {
|
|
460
|
+
io.err(`Nothing at "${reply2.path}" in ${formatStoreRef(reply2.store)}.`);
|
|
461
|
+
return finish(EXIT.failed);
|
|
462
|
+
}
|
|
463
|
+
io.out(`${formatState(reply2)}
|
|
464
|
+
`);
|
|
465
|
+
return finish(EXIT.ok);
|
|
466
|
+
}
|
|
467
|
+
const watch = await panel.request({ type: "SUBSCRIBE", selectors: [selector] }, ["SUBSCRIBED", "REJECTED"]);
|
|
468
|
+
if (watch.type === "REJECTED") {
|
|
469
|
+
io.err(`${watch.reason}: ${watch.unknown.join(", ")}`);
|
|
470
|
+
return finish(EXIT.failed);
|
|
471
|
+
}
|
|
472
|
+
const isWatched = ({ label, instance }) => watch.matched.some((store) => store.label === label && store.instance === instance);
|
|
473
|
+
const expectedAction = command.kind === "action" ? command.action : "setState";
|
|
474
|
+
const seen = [];
|
|
475
|
+
const outcome = new Promise((done) => {
|
|
476
|
+
const timer = setTimeout(() => done(null), options.timeoutSeconds * 1e3);
|
|
477
|
+
panel.onEvent((event2) => {
|
|
478
|
+
seen.push(event2);
|
|
479
|
+
if (event2.kind !== "action" || event2.action !== expectedAction || !isWatched(event2.store)) return;
|
|
480
|
+
clearTimeout(timer);
|
|
481
|
+
done(event2);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
const request = (() => {
|
|
485
|
+
if (command.kind === "set") return { type: "SET_STATE", selector, state: command.state };
|
|
486
|
+
if (command.kind === "patch") return { type: "PATCH_STATE", selector, patch: command.patch };
|
|
487
|
+
return { type: "RUN_ACTION", selector, action: command.action, args: command.args };
|
|
488
|
+
})();
|
|
489
|
+
const reply = await panel.request(request, ["DISPATCHED", "REQUEST_REJECTED"]);
|
|
490
|
+
if (reply.type === "REQUEST_REJECTED") {
|
|
491
|
+
io.err(reply.reason);
|
|
492
|
+
return finish(EXIT.failed);
|
|
493
|
+
}
|
|
494
|
+
const event = await outcome;
|
|
495
|
+
for (const other of seen) if (other !== event) io.out(`${formatEvent(other)}
|
|
496
|
+
|
|
497
|
+
`);
|
|
498
|
+
if (!event) {
|
|
499
|
+
io.err(
|
|
500
|
+
`Sent, but the page reported nothing within ${options.timeoutSeconds}s. The action may still be running, or the store is not live on the page.`
|
|
501
|
+
);
|
|
502
|
+
return finish(EXIT.noOutcome);
|
|
503
|
+
}
|
|
504
|
+
io.out(`${formatEvent(event)}
|
|
505
|
+
`);
|
|
506
|
+
return finish(event.kind === "action" && event.result && "error" in event.result ? EXIT.failed : EXIT.ok);
|
|
507
|
+
};
|
|
508
|
+
const start = async (panel) => {
|
|
509
|
+
const { stores } = await panel.request({ type: "GET_STORES" }, ["AVAILABLE_STORES"]);
|
|
510
|
+
if (options.list) {
|
|
511
|
+
io.out(`${formatStoreList(stores)}
|
|
512
|
+
`);
|
|
513
|
+
return finish(EXIT.ok);
|
|
514
|
+
}
|
|
515
|
+
let selectors;
|
|
516
|
+
if (options.targets.length) {
|
|
517
|
+
if (options.targets.includes(ALL_STORES)) {
|
|
518
|
+
io.err(ALL_WARNING);
|
|
519
|
+
selectors = ALL_STORES;
|
|
520
|
+
} else {
|
|
521
|
+
const resolved = resolveTargets(options.targets, stores);
|
|
522
|
+
if (resolved.unknown.length) {
|
|
523
|
+
for (const { target, candidates } of resolved.unknown) {
|
|
524
|
+
io.err(`Store "${target}" was not found. Available: ${candidates.join(", ") || "(none)"}`);
|
|
525
|
+
}
|
|
526
|
+
return finish(EXIT.failed);
|
|
527
|
+
}
|
|
528
|
+
selectors = resolved.selectors;
|
|
529
|
+
}
|
|
530
|
+
} else if (io.interactive && stores.length) {
|
|
531
|
+
const picked = await io.pick(stores);
|
|
532
|
+
if (!picked) return finish(EXIT.ok);
|
|
533
|
+
if ("all" in picked) {
|
|
534
|
+
io.err(ALL_WARNING);
|
|
535
|
+
selectors = ALL_STORES;
|
|
536
|
+
} else {
|
|
537
|
+
selectors = picked.selectors;
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
io.out(`${formatStoreList(stores)}
|
|
541
|
+
`);
|
|
542
|
+
io.err('\nPass --store <names> (or --store "*") to stream one or more of these stores.');
|
|
543
|
+
return finish(EXIT.needsTarget);
|
|
544
|
+
}
|
|
545
|
+
const rejected = await subscribe(panel, selectors);
|
|
546
|
+
if (rejected) {
|
|
547
|
+
io.err(`${rejected.reason}: ${rejected.unknown.join(", ")}`);
|
|
548
|
+
return finish(EXIT.failed);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const restore = async (panel, selectors) => {
|
|
552
|
+
for (let attempt = 1; attempt <= RESUBSCRIBE_ATTEMPTS; attempt++) {
|
|
553
|
+
if (current !== panel) return;
|
|
554
|
+
if (!await subscribe(panel, selectors)) return;
|
|
555
|
+
await wait(RESUBSCRIBE_DELAY_MS);
|
|
556
|
+
}
|
|
557
|
+
io.err("Could not restore the subscription: the stores are not known by DevTools any more.");
|
|
558
|
+
return finish(EXIT.failed);
|
|
559
|
+
};
|
|
560
|
+
server.onPanel((panel) => {
|
|
561
|
+
const isFirst = subscribed === null;
|
|
562
|
+
current = panel;
|
|
563
|
+
io.err(`Connected to the DevTools panel${describePanel(panel.hello) ? ` (${describePanel(panel.hello)})` : ""}.`);
|
|
564
|
+
if (!options.command) panel.onEvent((event) => io.out(`${formatEvent(event)}
|
|
565
|
+
|
|
566
|
+
`));
|
|
567
|
+
panel.onClose(() => {
|
|
568
|
+
if (current !== panel || finished) return;
|
|
569
|
+
current = null;
|
|
570
|
+
io.err("DevTools panel disconnected. Waiting for it to reconnect...");
|
|
571
|
+
});
|
|
572
|
+
if (options.command) {
|
|
573
|
+
if (commandStarted) return;
|
|
574
|
+
commandStarted = true;
|
|
575
|
+
}
|
|
576
|
+
const flow = options.command ? runCommand(panel, options.command) : isFirst ? start(panel) : restore(panel, subscribed);
|
|
577
|
+
flow.catch((error) => {
|
|
578
|
+
io.err(error.message);
|
|
579
|
+
return finish(EXIT.failed);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
io.err(
|
|
583
|
+
`Waiting for the DevTools panel on port ${options.port}. Open DevTools on the app tab and select the react-global-state-hooks panel.`
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// src/cli/select.ts
|
|
589
|
+
var ALL_ROW = "All stores";
|
|
590
|
+
var renderSelector = (stores, { cursor, marked }, rows) => {
|
|
591
|
+
const rowTitles = [
|
|
592
|
+
...stores.map((store2, index) => `${marked.has(index) ? "\u25C9" : " "} ${storeTitle(store2)}`),
|
|
593
|
+
` ${ALL_ROW}`
|
|
594
|
+
];
|
|
595
|
+
const chrome = 4;
|
|
596
|
+
const visible = rows ? Math.max(3, Math.min(rowTitles.length, Math.floor((rows - chrome) / 2))) : rowTitles.length;
|
|
597
|
+
const start = Math.min(Math.max(0, cursor - Math.floor(visible / 2)), rowTitles.length - visible);
|
|
598
|
+
const lines = ["Select a store (\u2191/\u2193 move, space mark several, enter confirm, q quit):", ""];
|
|
599
|
+
for (let index = start; index < start + visible; index++) {
|
|
600
|
+
lines.push(`${index === cursor ? "\u276F" : " "} ${rowTitles[index]}`);
|
|
601
|
+
}
|
|
602
|
+
lines.push("");
|
|
603
|
+
const store = stores[cursor];
|
|
604
|
+
if (!store) {
|
|
605
|
+
lines.push("Listen to every store. This is slower and noisier than picking specific stores.");
|
|
606
|
+
return lines;
|
|
607
|
+
}
|
|
608
|
+
const preview = [store.name ?? "unnamed"];
|
|
609
|
+
if (store.location) preview.push(store.location);
|
|
610
|
+
preview.push("", "State:", ...store.preview.split("\n"));
|
|
611
|
+
if (store.actions.length) preview.push("", "Actions:", ...store.actions);
|
|
612
|
+
const room = rows ? Math.max(0, rows - chrome - visible) : preview.length;
|
|
613
|
+
if (preview.length > room) return [...lines, ...preview.slice(0, Math.max(0, room - 1)), "\u2026"];
|
|
614
|
+
return [...lines, ...preview];
|
|
615
|
+
};
|
|
616
|
+
var KEYS = {
|
|
617
|
+
up: "\x1B[A",
|
|
618
|
+
down: "\x1B[B",
|
|
619
|
+
ctrlC: "",
|
|
620
|
+
enter: "\r",
|
|
621
|
+
space: " "
|
|
622
|
+
};
|
|
623
|
+
var pickStores = (stores) => new Promise((resolve) => {
|
|
624
|
+
const { stdin, stdout } = process;
|
|
625
|
+
const rowCount = stores.length + 1;
|
|
626
|
+
const view = { cursor: 0, marked: /* @__PURE__ */ new Set() };
|
|
627
|
+
const draw = () => {
|
|
628
|
+
const lines = renderSelector(stores, view, stdout.rows);
|
|
629
|
+
stdout.write(`\x1B[H${lines.map((line) => `\x1B[2K${line}`).join("\n")}\x1B[J`);
|
|
630
|
+
};
|
|
631
|
+
const finish = (selection) => {
|
|
632
|
+
stdin.off("data", onData);
|
|
633
|
+
stdin.setRawMode(false);
|
|
634
|
+
stdin.pause();
|
|
635
|
+
stdout.write("\x1B[?25h\x1B[?1049l");
|
|
636
|
+
resolve(selection);
|
|
637
|
+
};
|
|
638
|
+
const onData = (chunk) => {
|
|
639
|
+
const key = chunk.toString();
|
|
640
|
+
if (key === KEYS.ctrlC || key === "q" || key === "\x1B") return finish(null);
|
|
641
|
+
if (key === KEYS.up || key === "k") view.cursor = (view.cursor + rowCount - 1) % rowCount;
|
|
642
|
+
else if (key === KEYS.down || key === "j") view.cursor = (view.cursor + 1) % rowCount;
|
|
643
|
+
else if (key === KEYS.space && view.cursor < stores.length) {
|
|
644
|
+
if (!view.marked.delete(view.cursor)) view.marked.add(view.cursor);
|
|
645
|
+
} else if (key === KEYS.enter) {
|
|
646
|
+
if (view.cursor === stores.length) return finish({ all: true });
|
|
647
|
+
const picked = view.marked.size ? [...view.marked] : [view.cursor];
|
|
648
|
+
return finish({ selectors: picked.map((index) => stores[index].selector) });
|
|
649
|
+
}
|
|
650
|
+
draw();
|
|
651
|
+
};
|
|
652
|
+
stdout.write("\x1B[?1049h\x1B[?25l");
|
|
653
|
+
stdin.setRawMode(true);
|
|
654
|
+
stdin.resume();
|
|
655
|
+
stdin.on("data", onData);
|
|
656
|
+
draw();
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// src/cli/main.ts
|
|
660
|
+
var main = async () => {
|
|
661
|
+
let options;
|
|
662
|
+
try {
|
|
663
|
+
options = parseArgs(process.argv.slice(2));
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (!(error instanceof UsageError)) throw error;
|
|
666
|
+
process.stderr.write(`
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
${error.message}
|
|
670
|
+
|
|
671
|
+
${HELP}
|
|
672
|
+
`);
|
|
673
|
+
return EXIT.failed;
|
|
674
|
+
}
|
|
675
|
+
if (options.help) {
|
|
676
|
+
process.stdout.write(`
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
${HELP}
|
|
680
|
+
`);
|
|
681
|
+
return EXIT.ok;
|
|
682
|
+
}
|
|
683
|
+
const controller = new AbortController();
|
|
684
|
+
process.once("SIGINT", () => controller.abort());
|
|
685
|
+
process.once("SIGTERM", () => controller.abort());
|
|
686
|
+
process.stdout.on("error", () => controller.abort());
|
|
687
|
+
return run(
|
|
688
|
+
options,
|
|
689
|
+
{
|
|
690
|
+
out: (text) => void process.stdout.write(text),
|
|
691
|
+
err: (text) => void process.stderr.write(`${text}
|
|
692
|
+
`),
|
|
693
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
694
|
+
pick: pickStores
|
|
695
|
+
},
|
|
696
|
+
controller.signal
|
|
697
|
+
);
|
|
698
|
+
};
|
|
699
|
+
main().then(
|
|
700
|
+
(code) => process.exit(code),
|
|
701
|
+
(error) => {
|
|
702
|
+
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}
|
|
703
|
+
`);
|
|
704
|
+
process.exit(EXIT.failed);
|
|
705
|
+
}
|
|
706
|
+
);
|