@reporters/web 1.2.0 → 1.4.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/CHANGELOG.md +24 -0
- package/README.md +26 -0
- package/dist/{chunk-X6OHEYIN.js → chunk-KDWFRAWN.js} +24 -3
- package/dist/index.js +1 -1
- package/dist/sink.d.ts +4 -1
- package/dist/sink.js +2 -2
- package/dist/start.d.ts +24 -0
- package/dist/start.js +302 -0
- package/dist/viewer/index.html +62 -23
- package/dist/viewer.global.js +62 -23
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.0](https://github.com/MoLow/reporters/compare/web-v1.3.0...web-v1.4.0) (2026-07-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **tree-core:** report a passing todo as passed, reserving todo for failing ones ([#227](https://github.com/MoLow/reporters/issues/227)) ([46a9c87](https://github.com/MoLow/reporters/commit/46a9c874d88b2ae68aff80b50b286ea5e11e2a45))
|
|
9
|
+
* **web:** bounded log viewer — capped panels, full-log modal, named affordances ([#233](https://github.com/MoLow/reporters/issues/233)) ([f30d492](https://github.com/MoLow/reporters/commit/f30d49254394fb97f820b9202c055cd78b815000))
|
|
10
|
+
* **web:** clickable header status chips filter the tree ([#231](https://github.com/MoLow/reporters/issues/231)) ([bdf4f4e](https://github.com/MoLow/reporters/commit/bdf4f4e8efd3e8c03fe2c458a83d1d637b592f5f))
|
|
11
|
+
* **web:** distinct loading state before the first log fetch resolves ([#235](https://github.com/MoLow/reporters/issues/235)) ([5d1eb6c](https://github.com/MoLow/reporters/commit/5d1eb6ca77b3447a3304d8fc655eaf3a33d1c27f))
|
|
12
|
+
* **web:** richer diagnostics rendering in the viewer ([#232](https://github.com/MoLow/reporters/issues/232)) ([b7eb565](https://github.com/MoLow/reporters/commit/b7eb56506e5776a4231aef631a857512800df3ea))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **web:** keep the http server alive until the viewer reads the whole log ([#230](https://github.com/MoLow/reporters/issues/230)) ([ba7de4d](https://github.com/MoLow/reporters/commit/ba7de4d8035afb5ec4bece56b737cce607d0fa2c))
|
|
18
|
+
* **web:** show real wall-clock durations instead of summing concurrent tests ([#225](https://github.com/MoLow/reporters/issues/225)) ([e80ad8b](https://github.com/MoLow/reporters/commit/e80ad8b55a9c3b3aa2317763487f4185a0a5d9cf))
|
|
19
|
+
|
|
20
|
+
## [1.3.0](https://github.com/MoLow/reporters/compare/web-v1.2.0...web-v1.3.0) (2026-07-02)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* pluggable viewer report sources and custom s3 viewer links ([#222](https://github.com/MoLow/reporters/issues/222)) ([e553e89](https://github.com/MoLow/reporters/commit/e553e89a6e0c9ba9323483ca52a3142c8e70f8ce))
|
|
26
|
+
|
|
3
27
|
## [1.2.0](https://github.com/MoLow/reporters/compare/web-v1.1.0...web-v1.2.0) (2026-07-02)
|
|
4
28
|
|
|
5
29
|
|
package/README.md
CHANGED
|
@@ -60,3 +60,29 @@ The NDJSON is rendered by the tree viewer, reached three ways:
|
|
|
60
60
|
Built on the shared [`@reporters/tree-core`](https://github.com/MoLow/reporters/tree/main/packages/tree-core)
|
|
61
61
|
model (also used by [`@reporters/live`](https://github.com/MoLow/reporters/tree/main/packages/live)) —
|
|
62
62
|
the same run state, rendered in the browser instead of the terminal.
|
|
63
|
+
|
|
64
|
+
## Custom report sources (`@reporters/web/viewer`)
|
|
65
|
+
|
|
66
|
+
The hosted viewer reads `?src=<url>` with plain `fetch`. To serve reports that
|
|
67
|
+
need authentication (private buckets, SSO), build your own viewer page on the
|
|
68
|
+
same UI with a custom source resolver:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { startViewer } from '@reporters/web/viewer';
|
|
72
|
+
|
|
73
|
+
startViewer({
|
|
74
|
+
resolveSource: async (params) => {
|
|
75
|
+
if (params.get('src') || !params.get('key')) return null; // default handling
|
|
76
|
+
const credentials = await acquireCredentialsSomehow();
|
|
77
|
+
return { url: params.get('key')!, fetch: authenticatedFetch(credentials) };
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`resolveSource` runs before anything renders. Return `null`/`undefined` to fall
|
|
83
|
+
through to the default `?src=` handling; return `{ url, fetch?, pollMs? }` to
|
|
84
|
+
take over. The custom `fetch` receives the reader's `Range` header and must
|
|
85
|
+
return a standard `Response`; a thrown error shows the viewer's load-error
|
|
86
|
+
screen, and a promise that never resolves is fine while an auth redirect is in
|
|
87
|
+
flight. The export is a self-contained browser ESM module (React inlined) —
|
|
88
|
+
bundle it with your resolver into a static HTML page.
|
|
@@ -9,17 +9,27 @@ function viewerPage() {
|
|
|
9
9
|
return '<!doctype html><meta charset="utf-8"><p>viewer bundle missing \u2014 run the package build</p>';
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
var DRAIN_TIMEOUT_MS = 5e3;
|
|
13
|
+
function startViewerServer(host = "127.0.0.1", pollMs = 250, drainTimeoutMs = DRAIN_TIMEOUT_MS) {
|
|
13
14
|
const page = viewerPage();
|
|
14
15
|
let buffer = Buffer.alloc(0);
|
|
16
|
+
let sawViewer = false;
|
|
17
|
+
let servedUpTo = 0;
|
|
18
|
+
let onCaughtUp;
|
|
19
|
+
function served(upTo) {
|
|
20
|
+
servedUpTo = Math.max(servedUpTo, upTo);
|
|
21
|
+
if (servedUpTo >= buffer.length) onCaughtUp?.();
|
|
22
|
+
}
|
|
15
23
|
return new Promise((resolve) => {
|
|
16
24
|
const server = createServer((req, res) => {
|
|
17
25
|
const path = req.url.split("?")[0];
|
|
18
26
|
if (path === "/run.ndjson") {
|
|
27
|
+
sawViewer = true;
|
|
19
28
|
const range = /^bytes=(\d+)-/.exec(req.headers.range ?? "");
|
|
20
29
|
if (range) {
|
|
21
30
|
const startByte = Number(range[1]);
|
|
22
31
|
if (startByte >= buffer.length) {
|
|
32
|
+
served(startByte);
|
|
23
33
|
res.writeHead(416).end();
|
|
24
34
|
return;
|
|
25
35
|
}
|
|
@@ -29,10 +39,12 @@ function startViewerServer(host = "127.0.0.1", pollMs = 250) {
|
|
|
29
39
|
"content-range": `bytes ${startByte}-${buffer.length - 1}/${buffer.length}`
|
|
30
40
|
});
|
|
31
41
|
res.end(buffer.subarray(startByte));
|
|
42
|
+
served(buffer.length);
|
|
32
43
|
return;
|
|
33
44
|
}
|
|
34
45
|
res.writeHead(200, { "content-type": "application/x-ndjson", "accept-ranges": "bytes" });
|
|
35
46
|
res.end(buffer);
|
|
47
|
+
served(buffer.length);
|
|
36
48
|
return;
|
|
37
49
|
}
|
|
38
50
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
@@ -47,8 +59,17 @@ function startViewerServer(host = "127.0.0.1", pollMs = 250) {
|
|
|
47
59
|
push(chunk) {
|
|
48
60
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
49
61
|
},
|
|
50
|
-
close() {
|
|
51
|
-
|
|
62
|
+
async close() {
|
|
63
|
+
if (sawViewer && servedUpTo < buffer.length) {
|
|
64
|
+
await new Promise((res) => {
|
|
65
|
+
const timer = setTimeout(res, drainTimeoutMs);
|
|
66
|
+
onCaughtUp = () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
res();
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
await new Promise((res) => {
|
|
52
73
|
server.close(() => res());
|
|
53
74
|
});
|
|
54
75
|
}
|
package/dist/index.js
CHANGED
package/dist/sink.d.ts
CHANGED
|
@@ -19,12 +19,15 @@ interface HttpServerOptions {
|
|
|
19
19
|
host?: string;
|
|
20
20
|
/** Viewer polling cadence in ms (default 250; the viewer clamps to 100–10000). */
|
|
21
21
|
pollMs?: number;
|
|
22
|
+
/** How long close() keeps serving a lagging viewer before giving up (default 5000). */
|
|
23
|
+
drainTimeoutMs?: number;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* A sink that serves the run locally: the viewer page at `/` and the growing
|
|
25
27
|
* NDJSON at `/run.ndjson` (HTTP Range aware, so the viewer polls appended bytes
|
|
26
28
|
* — no `file://`/CORS limits). Stays alive after the run while attached to an
|
|
27
|
-
* interactive terminal; otherwise
|
|
29
|
+
* interactive terminal; otherwise drains a lagging viewer (bounded by
|
|
30
|
+
* `drainTimeoutMs`) and shuts down so the process can exit.
|
|
28
31
|
*/
|
|
29
32
|
declare function httpServer(opts?: HttpServerOptions): Sink;
|
|
30
33
|
|
package/dist/sink.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
startViewerServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KDWFRAWN.js";
|
|
4
4
|
|
|
5
5
|
// src/sink.ts
|
|
6
6
|
function httpServer(opts = {}) {
|
|
7
7
|
let server;
|
|
8
8
|
return {
|
|
9
9
|
async start() {
|
|
10
|
-
server = await startViewerServer(opts.host, opts.pollMs);
|
|
10
|
+
server = await startViewerServer(opts.host, opts.pollMs, opts.drainTimeoutMs);
|
|
11
11
|
},
|
|
12
12
|
write(chunk) {
|
|
13
13
|
server?.push(chunk);
|
package/dist/start.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type FetchLike = (url: string, init?: {
|
|
2
|
+
headers?: Record<string, string>;
|
|
3
|
+
}) => Promise<Response>;
|
|
4
|
+
|
|
5
|
+
interface ReportSource {
|
|
6
|
+
/** Passed to the NDJSON reader as the URL/identifier. */
|
|
7
|
+
url: string;
|
|
8
|
+
/** Transport for reads; defaults to the global fetch. Receives the reader's
|
|
9
|
+
* Range header and must return a standard Response. */
|
|
10
|
+
fetch?: FetchLike;
|
|
11
|
+
/** Poll cadence override; else resolved from ?poll= as today. */
|
|
12
|
+
pollMs?: number;
|
|
13
|
+
}
|
|
14
|
+
interface ViewerOptions {
|
|
15
|
+
/** Called first with the page's query params. Return a source to use it;
|
|
16
|
+
* return null/undefined to fall through to the default ?src= handling.
|
|
17
|
+
* A never-resolving promise is legitimate (e.g. an auth redirect is in
|
|
18
|
+
* flight). A thrown error shows the viewer's load-error screen. */
|
|
19
|
+
resolveSource?: (params: URLSearchParams) => Promise<ReportSource | null | undefined>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare function startViewer(options?: ViewerOptions): Promise<void>;
|
|
23
|
+
|
|
24
|
+
export { type FetchLike, type ReportSource, type ViewerOptions, startViewer };
|