@jsenv/core 41.4.1 → 41.4.2
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/dist/build/build.js +173 -4
- package/dist/build/jsenv_core_packages.js +91 -1
- package/dist/html/client_monitor_page.html +4 -1
- package/dist/html/clients_page.html +13 -5
- package/dist/js/client_reporter.js +185 -109
- package/dist/js/page_switcher.js +731 -0
- package/dist/start_dev_server/jsenv_core_packages.js +695 -4
- package/dist/start_dev_server/start_dev_server.js +357 -78
- package/package.json +2 -2
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +55 -5
- package/src/dev/start_dev_server.js +13 -3
- package/src/kitchen/kitchen.js +12 -3
- package/src/kitchen/url_graph/url_graph.js +11 -0
- package/src/plugins/client_monitoring/client/client_monitor_page.html +4 -1
- package/src/plugins/client_monitoring/client/client_reporter.js +185 -109
- package/src/plugins/client_monitoring/client/clients_page.html +13 -5
- package/src/plugins/client_monitoring/jsenv_plugin_client_monitoring.js +76 -68
- package/src/plugins/page_switcher/client/page_switcher.js +731 -0
- package/src/plugins/page_switcher/jsenv_plugin_page_switcher.js +42 -0
- package/src/plugins/protocol_file/html_pages.js +122 -0
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +24 -0
- package/src/plugins/resolution_node_esm/node_esm_resolver.js +5 -0
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* cooked one of our pages and reports back; we can only see clients that execute
|
|
7
7
|
* our injected script, not arbitrary HTTP clients of the dev server.
|
|
8
8
|
*
|
|
9
|
+
* The MAIN client — the machine the dev server runs on, browsing via
|
|
10
|
+
* localhost — is listed but not watched: it reports presence only (heartbeat,
|
|
11
|
+
* tabs), no console logs and no activity. Its devtools are already at hand,
|
|
12
|
+
* and the person reading the dashboard is that client. Watching is for the
|
|
13
|
+
* clients that reach the server over the network (a phone on the LAN address,
|
|
14
|
+
* acceptAnyIp: true), and every client record carries the ip it reports from —
|
|
15
|
+
* that ip is what tells the two kinds apart. See isLocalClient in
|
|
16
|
+
* client_reporter.js (the client side of the same rule).
|
|
17
|
+
*
|
|
9
18
|
* Transport reuses what the dev server already has instead of opening a second
|
|
10
19
|
* websocket:
|
|
11
20
|
* - server → clients uses the jsenv "server events" channel (the same websocket
|
|
@@ -42,8 +51,6 @@
|
|
|
42
51
|
*/
|
|
43
52
|
|
|
44
53
|
import { injectJsenvScript, parseHtml, stringifyHtmlAst } from "@jsenv/ast";
|
|
45
|
-
import { urlToRelativeUrl } from "@jsenv/urls";
|
|
46
|
-
import { readdirSync } from "node:fs";
|
|
47
54
|
import { getRuntimeFromRequest } from "../../dev/dev_server_plugins/runtime_from_request.js";
|
|
48
55
|
|
|
49
56
|
// Normalize the dev server's { runtimeName, runtimeVersion } to the { name,
|
|
@@ -79,6 +86,18 @@ const ACTIVITY_MAX_PER_CLIENT = 50;
|
|
|
79
86
|
const INACTIVITY_MS = 60 * 1000;
|
|
80
87
|
// A tab not heard from for this long is considered closed and dropped.
|
|
81
88
|
const TAB_TTL_MS = 2 * 60 * 1000;
|
|
89
|
+
// What a browser sends is not to be trusted with the server's memory: a log
|
|
90
|
+
// line is cut at the source too (see client_reporter.js), and cut again here so
|
|
91
|
+
// a hand-made POST cannot park megabytes in the buffer — which the
|
|
92
|
+
// server-events history would then keep a second time.
|
|
93
|
+
// A little above the client's own cut, so the "… (N more characters)" it adds
|
|
94
|
+
// survives this one — the reader needs to know something was left out.
|
|
95
|
+
const LOG_TEXT_MAX = 10_064;
|
|
96
|
+
// Clients seen since the server started, at most. One per browser profile in
|
|
97
|
+
// practice, but every private window and every cleared storage adds one that
|
|
98
|
+
// never comes back, each carrying its own buffer — so the oldest ones that are
|
|
99
|
+
// no longer online are let go.
|
|
100
|
+
const CLIENT_MAX = 50;
|
|
82
101
|
|
|
83
102
|
// The dev server already parses browser + version from a request (sec-ch-ua or
|
|
84
103
|
// user-agent) via getRuntimeFromRequest; it does not cover the OS, so this fills
|
|
@@ -119,7 +138,14 @@ const osFromUserAgent = (userAgent) => {
|
|
|
119
138
|
return { name: "unknown", version: "" };
|
|
120
139
|
};
|
|
121
140
|
|
|
122
|
-
|
|
141
|
+
// The machine the dev server runs on, talking to itself: the main client. A
|
|
142
|
+
// phone (or any other device) reaching the server over the network reports
|
|
143
|
+
// with the machine's LAN address instead — which is why the ip is kept on
|
|
144
|
+
// every client record: it is what tells the main client from the others.
|
|
145
|
+
const isLocalIp = (ip) =>
|
|
146
|
+
ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
147
|
+
|
|
148
|
+
export const jsenvPluginClientMonitoring = () => {
|
|
123
149
|
// id -> client record
|
|
124
150
|
const clients = new Map();
|
|
125
151
|
|
|
@@ -145,6 +171,9 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
145
171
|
userAgent,
|
|
146
172
|
runtime: runtimeFromRequest(request),
|
|
147
173
|
os: osFromUserAgent(userAgent),
|
|
174
|
+
// The address the reports come from; request.ipForwarded when a proxy
|
|
175
|
+
// sits in between, so the client's own address is kept, not the proxy's.
|
|
176
|
+
ip: request.ipForwarded || request.ip,
|
|
148
177
|
firstSeen: now(),
|
|
149
178
|
lastSeen: now(),
|
|
150
179
|
everSeen: false,
|
|
@@ -156,14 +185,38 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
156
185
|
tabs: new Map(),
|
|
157
186
|
};
|
|
158
187
|
clients.set(id, client);
|
|
159
|
-
} else
|
|
160
|
-
client.userAgent
|
|
161
|
-
|
|
162
|
-
|
|
188
|
+
} else {
|
|
189
|
+
if (userAgent && userAgent !== client.userAgent) {
|
|
190
|
+
client.userAgent = userAgent;
|
|
191
|
+
client.runtime = runtimeFromRequest(request);
|
|
192
|
+
client.os = osFromUserAgent(userAgent);
|
|
193
|
+
}
|
|
194
|
+
// A device changes address (wifi drop, DHCP): the record follows it.
|
|
195
|
+
const ip = request.ipForwarded || request.ip;
|
|
196
|
+
if (ip && ip !== client.ip) {
|
|
197
|
+
client.ip = ip;
|
|
198
|
+
}
|
|
163
199
|
}
|
|
164
200
|
return client;
|
|
165
201
|
};
|
|
166
202
|
|
|
203
|
+
// The oldest silent ones first: a client still reporting is one someone is
|
|
204
|
+
// looking at, whatever its age.
|
|
205
|
+
const pruneClients = () => {
|
|
206
|
+
if (clients.size <= CLIENT_MAX) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const droppable = [...clients.values()]
|
|
210
|
+
.filter((client) => !isOnline(client))
|
|
211
|
+
.sort((a, b) => a.lastSeen - b.lastSeen);
|
|
212
|
+
for (const client of droppable) {
|
|
213
|
+
if (clients.size <= CLIENT_MAX) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
clients.delete(client.id);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
167
220
|
const pruneLogs = (client) => {
|
|
168
221
|
const cutoff = now() - LOG_TTL_MS;
|
|
169
222
|
while (client.logs.length && client.logs[0].ts < cutoff) {
|
|
@@ -255,6 +308,10 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
255
308
|
// parsed { name, version } so pages can show a friendly browser/OS
|
|
256
309
|
runtime: client.runtime,
|
|
257
310
|
os: client.os,
|
|
311
|
+
ip: client.ip,
|
|
312
|
+
// The main client — the machine the dev server runs on, talking to
|
|
313
|
+
// itself over localhost.
|
|
314
|
+
local: isLocalIp(client.ip),
|
|
258
315
|
firstSeen: client.firstSeen,
|
|
259
316
|
lastSeen: client.lastSeen,
|
|
260
317
|
online: isOnline(client),
|
|
@@ -311,6 +368,7 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
311
368
|
|
|
312
369
|
updateTab(client, body.tab);
|
|
313
370
|
pruneTabs(client);
|
|
371
|
+
pruneClients();
|
|
314
372
|
|
|
315
373
|
if (firstEver) {
|
|
316
374
|
sendClientHere({ reason: "new", client: serializeClient(client) });
|
|
@@ -331,13 +389,22 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
331
389
|
for (const rawLog of logs) {
|
|
332
390
|
const entry = {
|
|
333
391
|
level: rawLog.level || "log",
|
|
334
|
-
text:
|
|
392
|
+
text:
|
|
393
|
+
typeof rawLog.text === "string"
|
|
394
|
+
? rawLog.text.slice(0, LOG_TEXT_MAX)
|
|
395
|
+
: "",
|
|
335
396
|
ts: rawLog.ts || now(),
|
|
336
397
|
};
|
|
337
398
|
// styled console segments ({ text, css } per %c run), when present, so a
|
|
338
399
|
// monitor can render colors; the plain text stays for copy/paste.
|
|
339
400
|
if (Array.isArray(rawLog.segments)) {
|
|
340
|
-
entry.segments = rawLog.segments
|
|
401
|
+
entry.segments = rawLog.segments.map((segment) => ({
|
|
402
|
+
...segment,
|
|
403
|
+
text:
|
|
404
|
+
typeof segment?.text === "string"
|
|
405
|
+
? segment.text.slice(0, LOG_TEXT_MAX)
|
|
406
|
+
: "",
|
|
407
|
+
}));
|
|
341
408
|
}
|
|
342
409
|
client.logs.push(entry);
|
|
343
410
|
sendClientLog({ clientId, ...entry });
|
|
@@ -361,57 +428,6 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
361
428
|
};
|
|
362
429
|
};
|
|
363
430
|
|
|
364
|
-
// The .html pages served under the source directory, as server-relative URLs,
|
|
365
|
-
// so the dashboard can offer them as "navigate this client to…" targets. Skips
|
|
366
|
-
// node_modules, build output and dot-dirs. Cached briefly — one scan per
|
|
367
|
-
// dialog-open is plenty; it needn't be fresh to the second.
|
|
368
|
-
const PAGE_SCAN_TTL_MS = 3000;
|
|
369
|
-
const PAGE_SCAN_SKIP_DIRS = new Set([
|
|
370
|
-
"node_modules",
|
|
371
|
-
"dist",
|
|
372
|
-
"git_ignored",
|
|
373
|
-
"old",
|
|
374
|
-
]);
|
|
375
|
-
let pageScanCache = null;
|
|
376
|
-
let pageScanAt = 0;
|
|
377
|
-
const listNavigablePages = () => {
|
|
378
|
-
if (!rootDirectoryUrl) {
|
|
379
|
-
return [];
|
|
380
|
-
}
|
|
381
|
-
if (pageScanCache && now() - pageScanAt < PAGE_SCAN_TTL_MS) {
|
|
382
|
-
return pageScanCache;
|
|
383
|
-
}
|
|
384
|
-
const pages = [];
|
|
385
|
-
const walk = (dirUrl) => {
|
|
386
|
-
let entries;
|
|
387
|
-
try {
|
|
388
|
-
entries = readdirSync(new URL(dirUrl), { withFileTypes: true });
|
|
389
|
-
} catch {
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
for (const entry of entries) {
|
|
393
|
-
const name = entry.name;
|
|
394
|
-
if (name[0] === ".") {
|
|
395
|
-
continue; // .git, .agents, dot-files…
|
|
396
|
-
}
|
|
397
|
-
if (entry.isDirectory()) {
|
|
398
|
-
if (!PAGE_SCAN_SKIP_DIRS.has(name)) {
|
|
399
|
-
walk(`${dirUrl}${name}/`);
|
|
400
|
-
}
|
|
401
|
-
} else if (name.endsWith(".html")) {
|
|
402
|
-
pages.push(
|
|
403
|
-
`/${urlToRelativeUrl(`${dirUrl}${name}`, rootDirectoryUrl)}`,
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
walk(String(rootDirectoryUrl));
|
|
409
|
-
pages.sort();
|
|
410
|
-
pageScanCache = pages;
|
|
411
|
-
pageScanAt = now();
|
|
412
|
-
return pages;
|
|
413
|
-
};
|
|
414
|
-
|
|
415
431
|
// Desktop pilots a client: validate a { clientId, tabId?, type, url? } command
|
|
416
432
|
// and broadcast it as a "client_command" server event. The reporter runs it
|
|
417
433
|
// only if the id (and tabId, when given) matches. type: "navigate" | "reload".
|
|
@@ -536,14 +552,6 @@ export const jsenvPluginClientMonitoring = ({ rootDirectoryUrl } = {}) => {
|
|
|
536
552
|
declarationSource: import.meta.url,
|
|
537
553
|
fetch: (request) => ingestCommand(request),
|
|
538
554
|
},
|
|
539
|
-
{
|
|
540
|
-
endpoint: "GET /.internal/clients/pages.json",
|
|
541
|
-
description:
|
|
542
|
-
"The .html pages under the source directory, offered as navigation targets for a client.",
|
|
543
|
-
availableMediaTypes: ["application/json"],
|
|
544
|
-
declarationSource: import.meta.url,
|
|
545
|
-
fetch: () => jsonResponse(listNavigablePages()),
|
|
546
|
-
},
|
|
547
555
|
{
|
|
548
556
|
endpoint: "GET /.internal/clients.json",
|
|
549
557
|
description: "Snapshot of every client seen since the server started.",
|