@reporters/mux 1.0.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/README.md +97 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +226 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
 [](https://codecov.io/gh/MoLow/reporters)
|
|
2
|
+
|
|
3
|
+
# Reporter Multiplexer
|
|
4
|
+
|
|
5
|
+
One `--test-reporter` flag, every environment covered.
|
|
6
|
+
|
|
7
|
+
`@reporters/mux` is environment-aware routing for `node:test` reporters.
|
|
8
|
+
Register it once; it reads a `mux.config` file, picks the active **profile**
|
|
9
|
+
(`ci` vs `local`, auto-detected), tees the test-event stream to every configured
|
|
10
|
+
**reporter**, and pipes each reporter's output into its **sink** —
|
|
11
|
+
`stdout`/`stderr`, a file, a local HTTP viewer, or a remote upload with a
|
|
12
|
+
shareable report URL.
|
|
13
|
+
|
|
14
|
+
The same command gives you a live interactive tree at your desk, and a CI log
|
|
15
|
+
plus a browsable report link on the build server:
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
That `report at` link is [`@reporters/web`](https://github.com/MoLow/reporters/tree/main/packages/web)'s
|
|
20
|
+
run being delivered through a sink — it opens as an interactive tree in the
|
|
21
|
+
browser (**[live demo](https://molow.github.io/reporters/?src=https://raw.githubusercontent.com/MoLow/reporters/5393ed7b104f42d90bb930ad89854d8fdff6785b/packages/web/assets/demo-run.ndjson)**):
|
|
22
|
+
|
|
23
|
+
[](https://molow.github.io/reporters/?src=https://raw.githubusercontent.com/MoLow/reporters/5393ed7b104f42d90bb930ad89854d8fdff6785b/packages/web/assets/demo-run.ndjson)
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node --test-reporter=@reporters/mux --test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Config
|
|
32
|
+
|
|
33
|
+
Create a `mux.config.js` (or `.mjs`/`.ts`), discovered by walking up from the
|
|
34
|
+
current directory. It default-exports a map of profile name → routes:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { httpServer } from '@reporters/web/sink';
|
|
38
|
+
import { gist } from '@reporters/sink';
|
|
39
|
+
import live from '@reporters/live';
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
local: [
|
|
43
|
+
{ reporter: live, sink: 'stdout' },
|
|
44
|
+
{ reporter: '@reporters/web', sink: httpServer() }, // serves localhost, opens the browser
|
|
45
|
+
],
|
|
46
|
+
ci: [
|
|
47
|
+
{ reporter: '@reporters/gh', sink: 'stdout' },
|
|
48
|
+
{ reporter: '@reporters/web', sink: gist() }, // uploads the run, links the hosted viewer
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both async-generator reporters (like `@reporters/live`, `@reporters/web`) and
|
|
54
|
+
Transform-stream reporters (like `@reporters/gh`) are supported as route reporters.
|
|
55
|
+
|
|
56
|
+
### Route fields
|
|
57
|
+
|
|
58
|
+
- `reporter` — a reporter function, a Transform/Duplex stream, or a module specifier `mux` will `import()`.
|
|
59
|
+
- `options` — optional 2nd-arg object passed to the reporter (e.g.
|
|
60
|
+
`@reporters/web` accepts `{ open: boolean }`); most reporters need none.
|
|
61
|
+
- `sink` — `'stdout'`, `'stderr'`, a file path, or a `Sink` object.
|
|
62
|
+
- `open` — open the sink's viewer URL in a browser. Defaults to on locally, off
|
|
63
|
+
in CI. `open: false` opts out; `REPORTERS_OPEN=1|0` forces it.
|
|
64
|
+
|
|
65
|
+
### Profile resolution
|
|
66
|
+
|
|
67
|
+
`REPORTERS_PROFILE` if set; otherwise `ci` when a CI environment is detected
|
|
68
|
+
(`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, …), else `local`. Profile
|
|
69
|
+
names are yours — add a `nightly` or `benchmark` profile and select it with
|
|
70
|
+
`REPORTERS_PROFILE=nightly`.
|
|
71
|
+
|
|
72
|
+
## Sinks
|
|
73
|
+
|
|
74
|
+
A sink decides where a reporter's bytes go — and, for viewers, where a human
|
|
75
|
+
looks:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
interface Sink {
|
|
79
|
+
start?(): Promise<void>;
|
|
80
|
+
write(chunk: string | Buffer): void | Promise<void>;
|
|
81
|
+
flush?(): Promise<void>;
|
|
82
|
+
close(): Promise<void>;
|
|
83
|
+
viewerUrl?(): string | undefined;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Built in: `'stdout'`, `'stderr'`, and file paths. Beyond those:
|
|
88
|
+
|
|
89
|
+
- [`@reporters/web`](https://github.com/MoLow/reporters/tree/main/packages/web)
|
|
90
|
+
ships `httpServer()` — serves the viewer on localhost over the growing run.
|
|
91
|
+
- [`@reporters/sink`](https://github.com/MoLow/reporters/tree/main/packages/sink)
|
|
92
|
+
ships `gist()` and `s3()` — upload the run where a browser can fetch it, so
|
|
93
|
+
the hosted viewer can render CI runs — plus `remoteSink()` to bring your own
|
|
94
|
+
transport.
|
|
95
|
+
|
|
96
|
+
When a sink exposes a `viewerUrl`, mux prints `report at <url>` to stderr and —
|
|
97
|
+
on GitHub Actions — adds a **View report** link to the job summary.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Duplex } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
type DiagnosticLevel = 'info' | 'warn' | 'error';
|
|
4
|
+
/** The fields of a `node:test` reporter event that the store consumes. */
|
|
5
|
+
interface TestEventData {
|
|
6
|
+
name?: string;
|
|
7
|
+
nesting?: number;
|
|
8
|
+
file?: string;
|
|
9
|
+
testId?: number;
|
|
10
|
+
parentId?: number;
|
|
11
|
+
line?: number;
|
|
12
|
+
column?: number;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
todo?: boolean | string;
|
|
15
|
+
skip?: boolean | string;
|
|
16
|
+
message?: string;
|
|
17
|
+
level?: DiagnosticLevel;
|
|
18
|
+
count?: number;
|
|
19
|
+
type?: 'suite' | 'test';
|
|
20
|
+
details?: {
|
|
21
|
+
duration_ms?: number;
|
|
22
|
+
error?: unknown;
|
|
23
|
+
type?: string;
|
|
24
|
+
passed?: boolean;
|
|
25
|
+
};
|
|
26
|
+
counts?: Record<string, number>;
|
|
27
|
+
duration_ms?: number;
|
|
28
|
+
success?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface TestEvent {
|
|
31
|
+
type: string;
|
|
32
|
+
data: TestEventData;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Sink {
|
|
36
|
+
/** Start servers/streams; resolves once ready to receive writes. */
|
|
37
|
+
start?(): Promise<void>;
|
|
38
|
+
write(chunk: string | Buffer): void | Promise<void>;
|
|
39
|
+
/** Periodic durability hook (e.g. a future s3 re-PUT). */
|
|
40
|
+
flush?(): Promise<void>;
|
|
41
|
+
/** The run ended; release resources. */
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
/** How a human views what was written, if anything. */
|
|
44
|
+
viewerUrl?(): string | undefined;
|
|
45
|
+
}
|
|
46
|
+
type SinkSpec = string | Sink;
|
|
47
|
+
|
|
48
|
+
/** A `node:test` reporter: pure transform from events to output bytes. */
|
|
49
|
+
type Reporter = (source: AsyncIterable<TestEvent>, options?: unknown) => AsyncGenerator<string>;
|
|
50
|
+
interface Route {
|
|
51
|
+
/** A reporter: a generator function, a Duplex/Transform stream instance, or a
|
|
52
|
+
* module specifier `mux` will `import()` to either shape. */
|
|
53
|
+
reporter: Reporter | Duplex | string;
|
|
54
|
+
/** Optional 2nd-arg passed to the reporter (only for output-format variants). */
|
|
55
|
+
options?: unknown;
|
|
56
|
+
/** Where the reporter's bytes go: 'stdout' | 'stderr' | a file path | a Sink. */
|
|
57
|
+
sink: SinkSpec;
|
|
58
|
+
/** Open the sink's viewer URL in a browser. Defaults to on locally, off in CI. */
|
|
59
|
+
open?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Map of profile name -> the routes active under that profile. */
|
|
62
|
+
type MuxConfig = Record<string, Route[]>;
|
|
63
|
+
|
|
64
|
+
/** A reporter function, a stream instance, or a module specifier to `import()`. */
|
|
65
|
+
declare function resolveReporter(reporter: Reporter | Duplex | string): Promise<Reporter | Duplex>;
|
|
66
|
+
/**
|
|
67
|
+
* Run every route in `config`'s active profile against `source`: tee the events,
|
|
68
|
+
* run each route's reporter, pipe its bytes into the route's sink, and open the
|
|
69
|
+
* sink's viewer URL when the gate allows. `open` is injected for testability.
|
|
70
|
+
*/
|
|
71
|
+
declare function runRoutes(source: AsyncIterable<TestEvent>, config: MuxConfig, env?: NodeJS.ProcessEnv, open?: (url: string) => void): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* The routing reporter. Reads `mux.config`, resolves the active profile, and
|
|
74
|
+
* fans the event stream out to each route's reporter + sink. It yields nothing
|
|
75
|
+
* to Node — all output flows through the sinks it drives directly.
|
|
76
|
+
*/
|
|
77
|
+
declare function mux(source: AsyncIterable<TestEvent>): AsyncGenerator<string>;
|
|
78
|
+
|
|
79
|
+
export { type MuxConfig, type Reporter, type Route, type Sink, type SinkSpec, mux as default, resolveReporter, runRoutes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Readable } from "stream";
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { pathToFileURL } from "url";
|
|
8
|
+
var CONFIG_NAMES = ["mux.config.js", "mux.config.mjs", "mux.config.ts"];
|
|
9
|
+
function findConfigFile(startDir = process.cwd()) {
|
|
10
|
+
let dir = startDir;
|
|
11
|
+
for (; ; ) {
|
|
12
|
+
for (const name of CONFIG_NAMES) {
|
|
13
|
+
const candidate = join(dir, name);
|
|
14
|
+
if (existsSync(candidate)) return candidate;
|
|
15
|
+
}
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir) return void 0;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function loadConfig(startDir = process.cwd()) {
|
|
22
|
+
const file = findConfigFile(startDir);
|
|
23
|
+
if (!file) throw new Error(`mux: no mux.config.{js,mjs,ts} found (searched up from ${startDir})`);
|
|
24
|
+
const mod = await import(pathToFileURL(file).href);
|
|
25
|
+
return mod.default ?? mod;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/profile.ts
|
|
29
|
+
function isCI(env = process.env) {
|
|
30
|
+
return Boolean(
|
|
31
|
+
env.CI || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS || env.GITLAB_CI || env.BUILDKITE
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function resolveProfileName(env = process.env) {
|
|
35
|
+
return env.REPORTERS_PROFILE || (isCI(env) ? "ci" : "local");
|
|
36
|
+
}
|
|
37
|
+
function resolveProfile(config, env = process.env) {
|
|
38
|
+
const name = resolveProfileName(env);
|
|
39
|
+
const routes = config[name];
|
|
40
|
+
if (!routes) {
|
|
41
|
+
const available = Object.keys(config).join(", ") || "none";
|
|
42
|
+
throw new Error(`mux: no profile "${name}" in config (available: ${available})`);
|
|
43
|
+
}
|
|
44
|
+
return routes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/broadcast.ts
|
|
48
|
+
var AsyncQueue = class {
|
|
49
|
+
values = [];
|
|
50
|
+
waiters = [];
|
|
51
|
+
done = false;
|
|
52
|
+
failed = false;
|
|
53
|
+
error;
|
|
54
|
+
push(value) {
|
|
55
|
+
const waiter = this.waiters.shift();
|
|
56
|
+
if (waiter) waiter.resolve({ value, done: false });
|
|
57
|
+
else this.values.push(value);
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
this.done = true;
|
|
61
|
+
let waiter = this.waiters.shift();
|
|
62
|
+
while (waiter) {
|
|
63
|
+
waiter.resolve({ value: void 0, done: true });
|
|
64
|
+
waiter = this.waiters.shift();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Surface `err` to consumers once their buffered values have drained. */
|
|
68
|
+
fail(err) {
|
|
69
|
+
this.failed = true;
|
|
70
|
+
this.error = err;
|
|
71
|
+
let waiter = this.waiters.shift();
|
|
72
|
+
while (waiter) {
|
|
73
|
+
waiter.reject(err);
|
|
74
|
+
waiter = this.waiters.shift();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
[Symbol.asyncIterator]() {
|
|
78
|
+
return {
|
|
79
|
+
next: () => {
|
|
80
|
+
if (this.values.length) return Promise.resolve({ value: this.values.shift(), done: false });
|
|
81
|
+
if (this.failed) return Promise.reject(this.error);
|
|
82
|
+
if (this.done) return Promise.resolve({ value: void 0, done: true });
|
|
83
|
+
return new Promise((resolve2, reject) => {
|
|
84
|
+
this.waiters.push({ resolve: resolve2, reject });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function broadcast(source, n) {
|
|
91
|
+
if (n === 0) return [];
|
|
92
|
+
const queues = Array.from({ length: n }, () => new AsyncQueue());
|
|
93
|
+
(async () => {
|
|
94
|
+
try {
|
|
95
|
+
for await (const item of source) for (const q of queues) q.push(item);
|
|
96
|
+
for (const q of queues) q.close();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
for (const q of queues) q.fail(err);
|
|
99
|
+
}
|
|
100
|
+
})();
|
|
101
|
+
return queues;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/sink.ts
|
|
105
|
+
import { createWriteStream } from "fs";
|
|
106
|
+
import { resolve } from "path";
|
|
107
|
+
function streamSink(stream) {
|
|
108
|
+
return {
|
|
109
|
+
write(chunk) {
|
|
110
|
+
stream.write(chunk);
|
|
111
|
+
},
|
|
112
|
+
async close() {
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function fileSink(path) {
|
|
117
|
+
const full = resolve(path);
|
|
118
|
+
let stream;
|
|
119
|
+
return {
|
|
120
|
+
async start() {
|
|
121
|
+
stream = createWriteStream(full);
|
|
122
|
+
},
|
|
123
|
+
write(chunk) {
|
|
124
|
+
(stream ?? (stream = createWriteStream(full))).write(chunk);
|
|
125
|
+
},
|
|
126
|
+
async close() {
|
|
127
|
+
const s = stream;
|
|
128
|
+
if (!s) return;
|
|
129
|
+
await new Promise((res, rej) => {
|
|
130
|
+
s.end((err) => {
|
|
131
|
+
if (err) return rej(err);
|
|
132
|
+
return res();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function resolveSink(spec) {
|
|
139
|
+
if (typeof spec !== "string") return spec;
|
|
140
|
+
if (spec === "stdout") return streamSink(process.stdout);
|
|
141
|
+
if (spec === "stderr") return streamSink(process.stderr);
|
|
142
|
+
return fileSink(spec);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/open.ts
|
|
146
|
+
import { spawn } from "child_process";
|
|
147
|
+
import { appendFileSync } from "fs";
|
|
148
|
+
function shouldOpen(routeOpen, env = process.env) {
|
|
149
|
+
const flag = env.REPORTERS_OPEN;
|
|
150
|
+
if (flag === "1" || flag === "true") return true;
|
|
151
|
+
if (flag === "0" || flag === "false") return false;
|
|
152
|
+
if (routeOpen === false) return false;
|
|
153
|
+
return !isCI(env);
|
|
154
|
+
}
|
|
155
|
+
function openCommand(url, platform = process.platform) {
|
|
156
|
+
if (platform === "darwin") return ["open", [url]];
|
|
157
|
+
if (platform === "win32") return ["cmd", ["/c", "start", "", url]];
|
|
158
|
+
return ["xdg-open", [url]];
|
|
159
|
+
}
|
|
160
|
+
function openInBrowser(url) {
|
|
161
|
+
const [cmd, args] = openCommand(url);
|
|
162
|
+
try {
|
|
163
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
164
|
+
child.on("error", () => {
|
|
165
|
+
});
|
|
166
|
+
child.unref();
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function announce(url, env = process.env) {
|
|
171
|
+
process.stderr.write(`
|
|
172
|
+
@reporters/mux: report at ${url}
|
|
173
|
+
`);
|
|
174
|
+
if (env.GITHUB_STEP_SUMMARY) {
|
|
175
|
+
try {
|
|
176
|
+
appendFileSync(env.GITHUB_STEP_SUMMARY, `
|
|
177
|
+
[View report](${url})
|
|
178
|
+
`);
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
var internals = { openInBrowser, announce };
|
|
184
|
+
|
|
185
|
+
// src/index.ts
|
|
186
|
+
function isStreamReporter(value) {
|
|
187
|
+
return typeof value === "object" && value !== null && typeof value.pipe === "function";
|
|
188
|
+
}
|
|
189
|
+
async function resolveReporter(reporter) {
|
|
190
|
+
if (typeof reporter === "function" || isStreamReporter(reporter)) return reporter;
|
|
191
|
+
const mod = await import(reporter);
|
|
192
|
+
const resolved = mod.default;
|
|
193
|
+
if (typeof resolved === "function" || isStreamReporter(resolved)) return resolved;
|
|
194
|
+
throw new Error(`mux: reporter "${reporter}" must default-export a generator function or a stream`);
|
|
195
|
+
}
|
|
196
|
+
async function runRoutes(source, config, env = process.env, open = internals.openInBrowser) {
|
|
197
|
+
const routes = resolveProfile(config, env);
|
|
198
|
+
const streams = broadcast(source, routes.length);
|
|
199
|
+
await Promise.all(routes.map(async (route, i) => {
|
|
200
|
+
const reporter = await resolveReporter(route.reporter);
|
|
201
|
+
const sink = resolveSink(route.sink);
|
|
202
|
+
const stage = typeof reporter === "function" ? (src) => reporter(src, route.options) : reporter;
|
|
203
|
+
try {
|
|
204
|
+
if (sink.start) await sink.start();
|
|
205
|
+
const url = sink.viewerUrl?.();
|
|
206
|
+
if (url) {
|
|
207
|
+
internals.announce(url, env);
|
|
208
|
+
if (shouldOpen(route.open, env)) open(url);
|
|
209
|
+
}
|
|
210
|
+
const output = Readable.from(streams[i]).compose(stage);
|
|
211
|
+
for await (const chunk of output) await sink.write(chunk);
|
|
212
|
+
if (sink.flush) await sink.flush();
|
|
213
|
+
} finally {
|
|
214
|
+
await sink.close();
|
|
215
|
+
}
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
async function* mux(source) {
|
|
219
|
+
const config = await loadConfig();
|
|
220
|
+
await runRoutes(source, config);
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
mux as default,
|
|
224
|
+
resolveReporter,
|
|
225
|
+
runRoutes
|
|
226
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reporters/mux",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Environment-aware routing reporter for `node:test`: tee the event stream to multiple reporters and sinks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"node:test",
|
|
8
|
+
"test",
|
|
9
|
+
"reporter",
|
|
10
|
+
"reporters",
|
|
11
|
+
"routing",
|
|
12
|
+
"multiplexer"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"./dist"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "rm -rf dist && tsup",
|
|
27
|
+
"prepack": "rm -rf dist && tsup",
|
|
28
|
+
"pretest": "rm -rf dist && tsup",
|
|
29
|
+
"test": "node --test-reporter=@reporters/mux --test"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@reporters/tree-core": "workspace:^",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"typescript": "^5.7.0"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/MoLow/reporters/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/MoLow/reporters/tree/main/packages/mux",
|
|
40
|
+
"repository": "https://github.com/MoLow/reporters.git",
|
|
41
|
+
"author": "Moshe Atlow",
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|