@uniflowed/test 0.0.0-alpha.9 → 0.2.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.
@@ -19,7 +19,7 @@
19
19
  // * `.only` anywhere in the file restricts the file to marked cases and their
20
20
  // ancestors; everything else is reported skipped, never silently dropped.
21
21
 
22
- import { firstUserSite } from "./frames.js";
22
+ import { callerSite, firstUserSite } from "./frames.js";
23
23
 
24
24
  /** The placeholders `it.each` substitutes a row into. */
25
25
  const ROW_TOKEN = /%[sjdi]/g;
@@ -27,16 +27,42 @@ const ROW_TOKEN = /%[sjdi]/g;
27
27
  /** What a test or hook body may return. */
28
28
  export type Body = () => mixed | Promise<mixed>;
29
29
 
30
+ /**
31
+ * One `beforeAll`, `afterAll`, `beforeEach` or `afterEach`.
32
+ *
33
+ * `timeoutMs` is the hook's own budget when its registration named one, and
34
+ * `null` when the hook runs under the budget of the case it belongs to. A hook
35
+ * that starts a process or a server needs longer than the cases it sets up for,
36
+ * and without a budget of its own the only lever was the file-wide timeout.
37
+ */
38
+ export type Hook = {| readonly body: Body, readonly timeoutMs: number | null |};
39
+
30
40
  /** The suffix written on a registration call. */
31
41
  export type Modifier = "none" | "only" | "skip" | "todo";
32
42
 
33
- /** One registered test case. */
43
+ /**
44
+ * How a benchmark runs under `uf test --bench`.
45
+ *
46
+ * `warmup` calls are made and their times thrown away, then `iterations` calls
47
+ * are each timed. `timeout` is the budget for one call, in milliseconds, and
48
+ * the benchmark as a whole is held to that budget once per call.
49
+ */
50
+ export type BenchOptions = {|
51
+ readonly warmup?: number,
52
+ readonly iterations?: number,
53
+ readonly timeout?: number,
54
+ |};
55
+
56
+ /** One registered test case, or one benchmark. */
34
57
  export type Case = {|
35
- readonly kind: "test",
58
+ readonly kind: "test" | "bench",
36
59
  readonly name: string,
37
60
  readonly body: Body | null,
38
61
  readonly modifier: Modifier,
62
+ readonly skipReason: string | null,
39
63
  readonly timeoutMs: number | null,
64
+ /** How to run it, for a benchmark; `null` for a test. */
65
+ readonly bench: BenchOptions | null,
40
66
  readonly line: number,
41
67
  readonly column: number,
42
68
  |};
@@ -47,10 +73,10 @@ export type Suite = {|
47
73
  readonly name: string,
48
74
  readonly modifier: Modifier,
49
75
  readonly children: Array<Suite | Case>,
50
- readonly beforeAll: Array<Body>,
51
- readonly afterAll: Array<Body>,
52
- readonly beforeEach: Array<Body>,
53
- readonly afterEach: Array<Body>,
76
+ readonly beforeAll: Array<Hook>,
77
+ readonly afterAll: Array<Hook>,
78
+ readonly beforeEach: Array<Hook>,
79
+ readonly afterEach: Array<Hook>,
54
80
  readonly line: number,
55
81
  readonly column: number,
56
82
  |};
@@ -102,6 +128,10 @@ export function collected(): Suite {
102
128
  * treats as "unknown" rather than as line one.
103
129
  */
104
130
  function callSite(): {| readonly line: number, readonly column: number |} {
131
+ const site = callerSite(callSite);
132
+ if (site !== undefined) {
133
+ return site ?? { line: 0, column: 0 };
134
+ }
105
135
  return firstUserSite(new Error("position").stack) ?? { line: 0, column: 0 };
106
136
  }
107
137
 
@@ -123,14 +153,18 @@ function addCase(
123
153
  body: Body | null,
124
154
  modifier: Modifier,
125
155
  timeoutMs: number | null,
156
+ skipReason: string | null = null,
157
+ bench: BenchOptions | null = null,
126
158
  ): void {
127
159
  const position = callSite();
128
160
  current.children.push({
129
- kind: "test",
161
+ kind: bench == null ? "test" : "bench",
130
162
  name,
131
163
  body,
132
164
  modifier,
165
+ skipReason,
133
166
  timeoutMs,
167
+ bench,
134
168
  line: position.line,
135
169
  column: position.column,
136
170
  });
@@ -179,6 +213,9 @@ function caseApi(): $FlowFixMe {
179
213
  api.skip = (name: string, body?: Body) => {
180
214
  addCase(name, body ?? null, "skip", null);
181
215
  };
216
+ api.skipBecause = (name: string, reason: string, body?: Body) => {
217
+ addCase(name, body ?? null, "skip", null, reason);
218
+ };
182
219
  api.todo = (name: string, body?: Body) => {
183
220
  addCase(name, body ?? null, "todo", null);
184
221
  };
@@ -211,6 +248,34 @@ export const it: $FlowFixMe = caseApi();
211
248
  /** `test` is `it`, for people who write it that way. */
212
249
  export const test: $FlowFixMe = it;
213
250
 
251
+ /** The `bench` API, and its modifiers. See [`suiteApi`] for the shape. */
252
+ function benchApi(): $FlowFixMe {
253
+ const api: $FlowFixMe = (name: string, body: Body, options?: BenchOptions) => {
254
+ addCase(name, body, "none", options?.timeout ?? null, null, options ?? {});
255
+ };
256
+ api.only = (name: string, body: Body, options?: BenchOptions) => {
257
+ addCase(name, body, "only", options?.timeout ?? null, null, options ?? {});
258
+ };
259
+ api.skip = (name: string, body?: Body) => {
260
+ addCase(name, body ?? null, "skip", null, null, {});
261
+ };
262
+ api.todo = (name: string) => {
263
+ addCase(name, null, "todo", null, null, {});
264
+ };
265
+ return api;
266
+ }
267
+
268
+ /**
269
+ * Register one benchmark.
270
+ *
271
+ * `uf test` reports a benchmark as skipped, so a suite does not pay for timing
272
+ * one, and `uf test --bench` runs the benchmarks in place of the tests and
273
+ * reports how long each call took. `bench.only`, `bench.skip` and `bench.todo`
274
+ * do what `it`'s do. See [`BenchOptions`] for `warmup`, `iterations` and
275
+ * `timeout`.
276
+ */
277
+ export const bench: $FlowFixMe = benchApi();
278
+
214
279
  /**
215
280
  * Substitute a row into a name, the way every runner spells it: `%s` for the
216
281
  * value, `%j` for its JSON.
@@ -225,22 +290,42 @@ function formatRow(name: string, row: mixed): string {
225
290
  });
226
291
  }
227
292
 
228
- /** Run once before the first test in this suite that runs. */
229
- export function beforeAll(body: Body): void {
230
- current.beforeAll.push(body);
293
+ /**
294
+ * A hook's own budget, from the second argument of its registration.
295
+ *
296
+ * `{ timeout }` is what `it` takes, and a bare number is what Jest and Vitest
297
+ * take — a suite moved from either writes `beforeAll(start, 30_000)`, and a
298
+ * budget that was silently dropped would fail as a timeout the file never
299
+ * asked for.
300
+ */
301
+ function hookTimeout(options: ?(TestOptions | number)): number | null {
302
+ if (typeof options === "number") {
303
+ return options;
304
+ }
305
+ return options?.timeout ?? null;
306
+ }
307
+
308
+ /**
309
+ * Run once before the first test in this suite that runs.
310
+ *
311
+ * When it fails, every case it was setting up for fails with its error: none
312
+ * of them runs against a setup that did not happen.
313
+ */
314
+ export function beforeAll(body: Body, options?: TestOptions | number): void {
315
+ current.beforeAll.push({ body, timeoutMs: hookTimeout(options) });
231
316
  }
232
317
 
233
318
  /** Run once after the last test in this suite that ran. */
234
- export function afterAll(body: Body): void {
235
- current.afterAll.push(body);
319
+ export function afterAll(body: Body, options?: TestOptions | number): void {
320
+ current.afterAll.push({ body, timeoutMs: hookTimeout(options) });
236
321
  }
237
322
 
238
323
  /** Run before every test in this suite and its children. */
239
- export function beforeEach(body: Body): void {
240
- current.beforeEach.push(body);
324
+ export function beforeEach(body: Body, options?: TestOptions | number): void {
325
+ current.beforeEach.push({ body, timeoutMs: hookTimeout(options) });
241
326
  }
242
327
 
243
328
  /** Run after every test in this suite and its children, including failures. */
244
- export function afterEach(body: Body): void {
245
- current.afterEach.push(body);
329
+ export function afterEach(body: Body, options?: TestOptions | number): void {
330
+ current.afterEach.push({ body, timeoutMs: hookTimeout(options) });
246
331
  }
package/internal/run.js CHANGED
@@ -13,11 +13,25 @@ import * as output from "./output.js";
13
13
  import * as snapshot from "./snapshot.js";
14
14
  import { AssertionError } from "./expect.js";
15
15
  import { type Site, firstUserSite, siteInFile, userFrames } from "./frames.js";
16
- import { type Body, type Case, type Suite, collected } from "./registry.js";
16
+ import {
17
+ type BenchOptions,
18
+ type Body,
19
+ type Case,
20
+ type Hook,
21
+ type Suite,
22
+ collected,
23
+ } from "./registry.js";
17
24
 
18
25
  /** How one case ended. */
19
26
  export type Outcome =
20
- | {| readonly status: "passed" |}
27
+ | {|
28
+ readonly status: "passed",
29
+ /**
30
+ * One timing per measured call, in whole microseconds, for a benchmark
31
+ * run under `uf test --bench`.
32
+ */
33
+ readonly samples?: $ReadOnlyArray<number>,
34
+ |}
21
35
  | {|
22
36
  readonly status: "failed",
23
37
  readonly message: string,
@@ -27,7 +41,11 @@ export type Outcome =
27
41
  /** Where the failing assertion was written, when the stack says. */
28
42
  readonly site: {| readonly line: number, readonly column: number |} | null,
29
43
  |}
30
- | {| readonly status: "skipped", readonly reason: "explicit" | "not-only" | "filtered" |}
44
+ | {|
45
+ readonly status: "skipped",
46
+ readonly reason: "explicit" | "not-only" | "filtered" | "bench" | "not-bench",
47
+ readonly message?: string | null,
48
+ |}
31
49
  | {| readonly status: "todo" |};
32
50
 
33
51
  /** One finished case, as the runner reports it. */
@@ -52,11 +70,31 @@ export type RunOptions = {|
52
70
  * which file that is — a test's name alone does not locate it.
53
71
  */
54
72
  readonly file?: string,
73
+ /**
74
+ * Run the benchmarks and report the tests skipped, rather than the other way
75
+ * round. `uf test --bench` sets it.
76
+ */
77
+ readonly bench?: boolean,
55
78
  |};
56
79
 
57
80
  /** Default budget for one case, matching what most runners use. */
58
81
  export const DEFAULT_TIMEOUT_MS: number = 5000;
59
82
 
83
+ /** Calls a benchmark makes and throws away before it times any, unless it says. */
84
+ export const DEFAULT_BENCH_WARMUP: number = 5;
85
+
86
+ /** Timed calls a benchmark makes, unless it says. */
87
+ export const DEFAULT_BENCH_ITERATIONS: number = 50;
88
+
89
+ /** Most timed calls one benchmark may ask for, and most samples `uf` keeps. */
90
+ export const MAX_BENCH_ITERATIONS: number = 100000;
91
+
92
+ /** Most untimed calls one benchmark may ask for first. */
93
+ export const MAX_BENCH_WARMUP: number = 10000;
94
+
95
+ /** The longest delay `setTimeout` honours; past it, the timer fires at once. */
96
+ const MAX_TIMER_MS = 2147483647;
97
+
60
98
  /** The separator between a suite's name and its child's. */
61
99
  export const NAME_SEPARATOR: string = " > ";
62
100
 
@@ -72,7 +110,9 @@ function fullName(path: $ReadOnlyArray<string>): string {
72
110
  */
73
111
  function hasOnly(node: Suite | Case, inherited: boolean): boolean {
74
112
  const marked = inherited || node.modifier === "only";
75
- if (node.kind === "test") {
113
+ // A benchmark is a case like a test: `bench.only` restricts a file as
114
+ // `it.only` does.
115
+ if (node.kind !== "suite") {
76
116
  return marked;
77
117
  }
78
118
  return node.children.some((child) => hasOnly(child, marked));
@@ -153,8 +193,8 @@ function failure(thrown: mixed, file: string | null): Outcome {
153
193
  /** Everything one case needs from the suites above it. */
154
194
  type Context = {|
155
195
  readonly path: $ReadOnlyArray<string>,
156
- readonly beforeEach: $ReadOnlyArray<Body>,
157
- readonly afterEach: $ReadOnlyArray<Body>,
196
+ readonly beforeEach: $ReadOnlyArray<Hook>,
197
+ readonly afterEach: $ReadOnlyArray<Hook>,
158
198
  readonly skipped: boolean,
159
199
  readonly onlyPath: boolean,
160
200
  |};
@@ -188,12 +228,17 @@ async function runCase(
188
228
  });
189
229
  };
190
230
 
191
- if (test.modifier === "todo" || test.body == null) {
231
+ if (test.modifier === "todo") {
192
232
  report({ status: "todo" });
193
233
  return true;
194
234
  }
195
235
  if (context.skipped || test.modifier === "skip") {
196
- report({ status: "skipped", reason: "explicit" });
236
+ report({ status: "skipped", reason: "explicit", message: test.skipReason });
237
+ return true;
238
+ }
239
+ const body = test.body;
240
+ if (body == null) {
241
+ report({ status: "todo" });
197
242
  return true;
198
243
  }
199
244
  if (!context.onlyPath) {
@@ -205,9 +250,19 @@ async function runCase(
205
250
  report({ status: "skipped", reason: "filtered" });
206
251
  return true;
207
252
  }
253
+ // A run is the tests or the benchmarks, never both. A benchmark timed inside
254
+ // an ordinary run would slow every suite that has one, beside workers busy
255
+ // with other files; a test inside a run of benchmarks would be timed with
256
+ // them.
257
+ const benchmark = test.kind === "bench";
258
+ if (benchmark !== (options.bench === true)) {
259
+ report({ status: "skipped", reason: benchmark ? "bench" : "not-bench" });
260
+ return true;
261
+ }
208
262
 
209
263
  const timeoutMs = test.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
210
264
  let outcome: Outcome = { status: "passed" };
265
+ let samples: $ReadOnlyArray<number> | null = null;
211
266
  // Setup, body and teardown run *inside* the case's output context, and that
212
267
  // nesting is the whole of the fix for #207. What names a printed line is no
213
268
  // longer where the runner had got to when the line arrived — which named the
@@ -220,9 +275,13 @@ async function runCase(
220
275
  await output.runInTest(name, async () => {
221
276
  try {
222
277
  for (const hook of context.beforeEach) {
223
- await withTimeout(hook, timeoutMs);
278
+ await withTimeout(hook.body, hook.timeoutMs ?? timeoutMs);
279
+ }
280
+ if (benchmark) {
281
+ samples = await measure(body, test.bench, timeoutMs);
282
+ } else {
283
+ await withTimeout(body, timeoutMs);
224
284
  }
225
- await withTimeout(test.body, timeoutMs);
226
285
  } catch (thrown) {
227
286
  outcome = failure(thrown, options.file ?? null);
228
287
  }
@@ -230,7 +289,7 @@ async function runCase(
230
289
  // when the body had not already failed.
231
290
  for (const hook of context.afterEach) {
232
291
  try {
233
- await withTimeout(hook, timeoutMs);
292
+ await withTimeout(hook.body, hook.timeoutMs ?? timeoutMs);
234
293
  } catch (thrown) {
235
294
  if (outcome.status === "passed") {
236
295
  outcome = failure(thrown, options.file ?? null);
@@ -243,10 +302,74 @@ async function runCase(
243
302
  // is the file's, and a snapshot taken outside one fails with something better
244
303
  // than a key belonging to whichever test happened to run last.
245
304
  snapshot.exitTest();
246
- report(outcome);
305
+ report(outcome.status === "passed" && samples != null ? { status: "passed", samples } : outcome);
247
306
  return outcome.status !== "failed";
248
307
  }
249
308
 
309
+ /**
310
+ * Time a benchmark's body: `warmup` calls thrown away, then `iterations` timed.
311
+ *
312
+ * The body is called directly and awaited, with nothing else between the two
313
+ * readings of the clock: a timer set around every call would be timed with
314
+ * it, and on a body that takes microseconds that is most of the number. The
315
+ * budget is held around the whole loop instead, at one call's budget per
316
+ * call, so a benchmark that hangs still fails rather than holding the worker.
317
+ */
318
+ async function measure(
319
+ body: Body,
320
+ options: BenchOptions | null,
321
+ timeoutMs: number,
322
+ ): Promise<$ReadOnlyArray<number>> {
323
+ const warmup = count("warmup", options?.warmup, DEFAULT_BENCH_WARMUP, 0, MAX_BENCH_WARMUP);
324
+ const iterations = count(
325
+ "iterations",
326
+ options?.iterations,
327
+ DEFAULT_BENCH_ITERATIONS,
328
+ 1,
329
+ MAX_BENCH_ITERATIONS,
330
+ );
331
+ const rounds = warmup + iterations;
332
+ const samples: Array<number> = [];
333
+ await withTimeout(
334
+ async () => {
335
+ for (let round = 0; round < rounds; round += 1) {
336
+ const started = performance.now();
337
+ await body();
338
+ if (round >= warmup) {
339
+ samples.push(Math.round((performance.now() - started) * 1000));
340
+ }
341
+ }
342
+ },
343
+ Math.min(timeoutMs * rounds, MAX_TIMER_MS),
344
+ );
345
+ return samples;
346
+ }
347
+
348
+ /**
349
+ * A benchmark option that has to be a whole number from `least` to `most`.
350
+ *
351
+ * Refused rather than clamped: `iterations: 0` or `warmup: 1e9` is a mistake,
352
+ * and a benchmark quietly run some other number of times reports numbers
353
+ * about a run nobody asked for.
354
+ */
355
+ function count(
356
+ option: string,
357
+ value: ?number,
358
+ fallback: number,
359
+ least: number,
360
+ most: number,
361
+ ): number {
362
+ if (value == null) {
363
+ return fallback;
364
+ }
365
+ if (!Number.isInteger(value) || value < least || value > most) {
366
+ throw new Error(
367
+ `bench \`${option}\` has to be a whole number from ${least} to ${most}, and was ${String(value)}`,
368
+ );
369
+ }
370
+ return value;
371
+ }
372
+
250
373
  /**
251
374
  * Walk one suite, running what it contains.
252
375
  *
@@ -280,16 +403,25 @@ async function runSuite(
280
403
  // every `beforeAll` in an ordinary file — one where the tests live inside a
281
404
  // `describe` — never running at all, silently. The chain is walked outermost
282
405
  // first, so an inner suite's setup sees what the outer one did.
406
+ //
407
+ // Once, and remembered either way. The outcome is kept rather than a "done"
408
+ // flag, because a flag raised before the hooks ran let every case after the
409
+ // first go ahead when setup had failed: the first case reported the hook's
410
+ // error and the rest ran against a fixture that was never made, failing — or
411
+ // worse, passing — for reasons that had nothing to do with them.
283
412
  let setUp = false;
284
- const setUpOnce = async () => {
285
- if (setUp) {
286
- return;
287
- }
288
- await setUpAncestors();
289
- setUp = true;
290
- for (const hook of node.beforeAll) {
291
- await withTimeout(hook, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
413
+ let setup: Promise<void> | null = null;
414
+ const setUpOnce = (): Promise<void> => {
415
+ if (setup == null) {
416
+ setUp = true;
417
+ setup = (async () => {
418
+ await setUpAncestors();
419
+ for (const hook of node.beforeAll) {
420
+ await withTimeout(hook.body, hook.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
421
+ }
422
+ })();
292
423
  }
424
+ return setup;
293
425
  };
294
426
 
295
427
  let passed = true;
@@ -297,13 +429,16 @@ async function runSuite(
297
429
  if (state.bail) {
298
430
  break;
299
431
  }
300
- if (child.kind === "test") {
432
+ if (child.kind !== "suite") {
301
433
  const willRun =
302
434
  !inner.skipped &&
303
435
  child.modifier !== "skip" &&
304
436
  child.modifier !== "todo" &&
305
437
  child.body != null &&
306
- (!onlyMode || inner.onlyPath || child.modifier === "only");
438
+ (!onlyMode || inner.onlyPath || child.modifier === "only") &&
439
+ // A benchmark in a run of the tests, or a test in a run of the
440
+ // benchmarks, is reported skipped and sets nothing up.
441
+ (child.kind === "bench") === (options.bench === true);
307
442
  if (willRun) {
308
443
  try {
309
444
  await setUpOnce();
@@ -315,7 +450,7 @@ async function runSuite(
315
450
  line: child.line,
316
451
  column: child.column,
317
452
  durationMicros: 0,
318
- outcome: failure(thrown),
453
+ outcome: failure(thrown, options.file ?? null),
319
454
  });
320
455
  passed = false;
321
456
  continue;
@@ -336,7 +471,7 @@ async function runSuite(
336
471
  if (setUp) {
337
472
  for (const hook of node.afterAll) {
338
473
  try {
339
- await withTimeout(hook, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
474
+ await withTimeout(hook.body, hook.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
340
475
  } catch {
341
476
  // A teardown failure cannot fail a test that already reported, and
342
477
  // there is nothing left to attach it to; the file's own status carries
@@ -82,7 +82,7 @@ function host(): $FlowFixMe {
82
82
  * formats a date sees a plausible one — and `setSystemTime` is how a test that
83
83
  * cares says which.
84
84
  */
85
- export function useFakeTimers(): void {
85
+ export function installFakeClock(): void {
86
86
  if (installed != null) {
87
87
  return;
88
88
  }
@@ -120,7 +120,7 @@ export function useFakeTimers(): void {
120
120
  }
121
121
 
122
122
  /** Put the real scheduling globals back. */
123
- export function useRealTimers(): void {
123
+ export function restoreRealClock(): void {
124
124
  if (installed == null) {
125
125
  return;
126
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/test",
3
- "version": "0.0.0-alpha.9",
3
+ "version": "0.2.0",
4
4
  "description": "The test API and worker for `uf test`: describe/it, a full matcher set, and the process uf fans test files out to.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,16 +11,52 @@
11
11
  "directory": "packages/test"
12
12
  },
13
13
  "exports": {
14
- ".": "./index.js",
14
+ ".": {
15
+ "uniflowed-bun-test": "./bun/index.js",
16
+ "default": "./index.js"
17
+ },
18
+ "./in-source": "./in-source.js",
15
19
  "./worker": "./worker.js",
16
- "./package.json": "./package.json"
20
+ "./browser-worker": "./browser-worker.js",
21
+ "./package.json": "./package.json",
22
+ "./app": {
23
+ "browser": "./app-browser.js",
24
+ "default": "./app.js"
25
+ },
26
+ "./browser": "./browser.js"
17
27
  },
18
28
  "files": [
29
+ "browser-worker.js",
30
+ "bun",
31
+ "in-source.js",
19
32
  "index.js",
33
+ "internal",
20
34
  "worker.js",
21
- "internal"
35
+ "app.js",
36
+ "browser.js",
37
+ "app-browser.js",
38
+ "!*.test.js"
22
39
  ],
23
40
  "dependencies": {
24
- "@uniflowed/host": "0.0.0-alpha.9"
41
+ "@uniflowed/host": "0.2.0",
42
+ "pixelmatch": "^7.1.0",
43
+ "pngjs": "^7.0.0"
44
+ },
45
+ "peerDependencies": {
46
+ "axe-core": ">=4"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "axe-core": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "browser": {
54
+ "node:async_hooks": "./internal/browser/node.js",
55
+ "node:fs": "./internal/browser/node.js",
56
+ "node:module": "./internal/browser/node.js",
57
+ "node:path": "./internal/browser/node.js",
58
+ "node:url": "./internal/browser/node.js",
59
+ "node:util": "./internal/browser/node.js",
60
+ "./internal/browser/transport.js": "./internal/browser/page-transport.js"
25
61
  }
26
62
  }