monocart-coverage-reports 2.13.1 → 3.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.
Files changed (68) hide show
  1. package/README.md +267 -114
  2. package/README.zh-Hans.md +268 -115
  3. package/lib/assets.js +6 -6
  4. package/lib/cli.js +191 -95
  5. package/lib/client/cdp-client.js +74 -43
  6. package/lib/client/coverage-client.js +24 -8
  7. package/lib/client/ws-session.js +115 -37
  8. package/lib/converter/ast-visitor.js +5 -7
  9. package/lib/converter/ast.js +6 -6
  10. package/lib/converter/collect-source-maps.js +119 -92
  11. package/lib/converter/converter.js +49 -117
  12. package/lib/converter/fake-source-coverage.js +64 -0
  13. package/lib/converter/find-original-range.js +3 -3
  14. package/lib/converter/flatten-source-maps.js +2 -4
  15. package/lib/converter/ignore.js +2 -4
  16. package/lib/converter/info-branch.js +5 -3
  17. package/lib/converter/info-function.js +5 -3
  18. package/lib/converter/info-statement.js +5 -3
  19. package/lib/converter/range-index.js +78 -0
  20. package/lib/converter/source-range-merge.js +103 -0
  21. package/lib/converter/untested.js +27 -55
  22. package/lib/default/options.js +13 -1
  23. package/lib/generate.js +44 -26
  24. package/lib/index.d.ts +52 -4
  25. package/lib/index.js +35 -41
  26. package/lib/istanbul/istanbul-summary.js +2 -2
  27. package/lib/istanbul/istanbul.js +35 -13
  28. package/lib/packages/chunks/chunk-RVOSNMVU.js +2 -0
  29. package/lib/packages/monocart-coverage-assets.js +4 -3
  30. package/lib/packages/monocart-coverage-converter.js +27 -0
  31. package/lib/packages/monocart-coverage-vendor.js +2 -41
  32. package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
  33. package/lib/platform/concurrency.js +47 -43
  34. package/lib/platform/share.js +1 -1
  35. package/lib/register/hooks.js +19 -13
  36. package/lib/register/register.js +5 -3
  37. package/lib/reports/codacy.js +3 -5
  38. package/lib/reports/codecov.js +3 -5
  39. package/lib/reports/console-details.js +5 -5
  40. package/lib/reports/console-summary.js +4 -4
  41. package/lib/reports/custom.js +8 -24
  42. package/lib/reports/markdown-details.js +4 -6
  43. package/lib/reports/markdown-summary.js +4 -6
  44. package/lib/reports/raw.js +10 -12
  45. package/lib/reports/v8-json.js +3 -5
  46. package/lib/reports/v8.js +3 -5
  47. package/lib/utils/dedupe.js +1 -1
  48. package/lib/utils/gc.js +30 -14
  49. package/lib/utils/markdown.js +2 -2
  50. package/lib/utils/merge/merge.js +2 -4
  51. package/lib/utils/merge/range-tree.js +1 -1
  52. package/lib/utils/request.js +85 -73
  53. package/lib/utils/snapshot.js +4 -4
  54. package/lib/utils/source-path.js +5 -5
  55. package/lib/utils/util.js +61 -41
  56. package/lib/v8/v8-summary.js +2 -4
  57. package/lib/v8/v8.js +11 -11
  58. package/migrate-to-3.md +175 -0
  59. package/package.json +153 -149
  60. package/skills/monocart-coverage-reports/SKILL.md +106 -0
  61. package/skills/monocart-coverage-reports/references/cli.md +108 -0
  62. package/skills/monocart-coverage-reports/references/collection.md +152 -0
  63. package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
  64. package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
  65. package/skills/monocart-coverage-reports/references/repository-map.md +55 -0
  66. package/lib/index.mjs +0 -9
  67. package/lib/register/hooks.mjs +0 -3
  68. package/lib/register/register.mjs +0 -3
package/lib/assets.js CHANGED
@@ -1,8 +1,8 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { deflateSync } = require('lz-utils');
4
- const Util = require('./utils/util.js');
5
- const assetsMap = require('./packages/monocart-coverage-assets.js');
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { deflateSync } from 'lz-utils';
4
+ import { Util } from './utils/util.js';
5
+ import { assetsMap } from './packages/monocart-coverage-assets.js';
6
6
 
7
7
  const Assets = {
8
8
 
@@ -95,4 +95,4 @@ const Assets = {
95
95
  }
96
96
  };
97
97
 
98
- module.exports = Assets;
98
+ export { Assets };
package/lib/cli.js CHANGED
@@ -1,29 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const path = require('path');
4
- const { pathToFileURL } = require('url');
5
- const EC = require('eight-colors');
3
+ import path from 'node:path';
4
+ import { constants } from 'node:os';
5
+ import { pathToFileURL } from 'node:url';
6
+ import EC from 'eight-colors';
6
7
 
7
- const { program } = require('commander');
8
- const { foregroundChild } = require('foreground-child');
8
+ import { program } from 'commander';
9
+ import { execa } from 'execa';
9
10
 
10
- const MCR = require('./index.js');
11
- const Util = require('./utils/util.js');
11
+ import { MCR } from './index.js';
12
+ import { Util } from './utils/util.js';
13
+ import pkg from '../package.json' with { type: 'json' };
12
14
 
13
- const version = require('../package.json').version;
15
+ const version = pkg.version;
14
16
 
15
17
  // quote NODE_OPTIONS argument if it contains whitespace so child processes
16
18
  // inheriting the env var can parse it correctly
17
- const quoteNodeOption = (v) => ((/\s/).test(v) ? `"${v}"` : v);
19
+ const quoteNodeOption = (v) => ((/[\s"\\]/).test(v) ? JSON.stringify(v) : v);
18
20
 
19
21
  // absolute path so NODE_OPTIONS keeps resolving when child processes are
20
22
  // spawned with a different cwd than the mcr parent process
21
- const getRequirePath = () => {
22
- return quoteNodeOption(path.resolve(__dirname, 'register', 'register.js'));
23
- };
24
-
25
23
  const getImportPath = () => {
26
- return quoteNodeOption(pathToFileURL(path.resolve(__dirname, 'register', 'register.mjs')).href);
24
+ return quoteNodeOption(pathToFileURL(path.resolve(import.meta.dirname, 'register', 'register.js')).href);
27
25
  };
28
26
 
29
27
  const getInitNodeOptions = async (cliOptions) => {
@@ -32,79 +30,34 @@ const getInitNodeOptions = async (cliOptions) => {
32
30
  nodeOptions.push(process.env.NODE_OPTIONS);
33
31
  }
34
32
 
35
- if (cliOptions.import) {
36
- nodeOptions.push(`--import ${cliOptions.import}`);
37
- // for load mcr.config.ts
38
- await import(cliOptions.import);
39
- } else if (cliOptions.require) {
40
- nodeOptions.push(`--require ${cliOptions.require}`);
41
- // for load mcr.config.ts
42
- await import(cliOptions.require);
33
+ const name = cliOptions.import || cliOptions.require;
34
+ if (name) {
35
+ const specifier = path.isAbsolute(name) || name.startsWith('.') ? pathToFileURL(path.resolve(name)).href : import.meta.resolve(name);
36
+ nodeOptions.push(`--import ${quoteNodeOption(specifier)}`);
37
+ // Load before mcr.config.ts. CJS interoperability is handled by Node.
38
+ await import(specifier);
43
39
  }
44
40
 
45
41
  return nodeOptions;
46
42
  };
47
43
 
48
- const getPreloadType = (nodeOptions) => {
49
- const hasImport = nodeOptions.find((it) => it.includes('--import'));
50
- if (hasImport) {
51
- return '--import';
52
- }
53
- return '--require';
54
- };
55
-
56
- const checkRegisterFeature = () => {
57
- const nv = process.versions.node;
58
-
59
- // "module.register" added in Node.js: v20.6.0
60
- // if (Util.cmpVersion(nv, '20.6.0') >= 0) {
61
- // return true;
62
- // }
63
- // but also added in: v18.19.0
64
- const requiredNV = '18.19.0';
65
- if (Util.cmpVersion(nv, requiredNV) < 0) {
66
- Util.logInfo(`The current Node.js version "${nv}" does NOT support "module.register", it requires "${requiredNV}" or higher.`);
67
- return false;
68
- }
69
-
70
- // could be < 20.6.0 but just ignore it, please using latest minor version
71
-
72
- return true;
73
- };
74
-
75
44
  const loadEnv = (cliOptions) => {
76
45
  if (!cliOptions.env) {
77
46
  return;
78
47
  }
79
48
  const envFile = cliOptions.env === true ? '.env' : cliOptions.env;
80
- const loadEnvFile = process.loadEnvFile;
81
- if (typeof loadEnvFile === 'function') {
82
- loadEnvFile(envFile);
83
- }
49
+ process.loadEnvFile(envFile);
84
50
  };
85
51
 
86
52
  const initNodeOptions = async (cliOptions) => {
87
53
 
88
54
  loadEnv(cliOptions);
89
55
 
90
- const supportRegister = checkRegisterFeature();
91
- if (!supportRegister) {
92
- return;
93
- }
94
-
95
56
  const nodeOptions = await getInitNodeOptions(cliOptions);
96
57
  // console.log(nodeOptions);
97
58
 
98
- const preloadType = getPreloadType(nodeOptions);
99
-
100
- // export source after
101
- if (preloadType === '--import') {
102
- const importPath = getImportPath();
103
- nodeOptions.unshift(`--import ${importPath}`);
104
- } else {
105
- const requirePath = getRequirePath();
106
- nodeOptions.unshift(`--require ${requirePath}`);
107
- }
59
+ // Register source capture before other ESM preloads.
60
+ nodeOptions.unshift(`--import ${getImportPath()}`);
108
61
 
109
62
  // console.log(nodeOptions);
110
63
  const nodeOptionsStr = nodeOptions.join(' ');
@@ -132,9 +85,88 @@ const mergeCoverage = async (cliOptions) => {
132
85
  await coverageReport.generate();
133
86
  };
134
87
 
135
- const executeCommand = async (command, cliOptions) => {
88
+ // Only check whether targets were supplied. Node owns all target parsing,
89
+ // discovery and validation; preserve each argument exactly as provided.
90
+ const getTestTargets = (test) => {
91
+ if (!test || (Array.isArray(test) && !test.length)) {
92
+ Util.logInfo('No test targets configured; skipping tests.');
93
+ return;
94
+ }
95
+
96
+ return Array.isArray(test) ? test : [test];
97
+ };
136
98
 
137
- Util.logInfo(`Execute: ${EC.cyan(command)}`);
99
+ // Preserve signals and failing commands even if a hook sets exitCode=0.
100
+ const getExitStatus = (code, signal) => signal || code || process.exitCode || 0;
101
+
102
+ const applyExitStatus = (status) => {
103
+ if (typeof status === 'number') {
104
+ process.exitCode = status;
105
+ return;
106
+ }
107
+ process.exitCode = 128 + (constants.signals[status] || 1);
108
+ if (process.platform !== 'win32') {
109
+ // Stay alive until the signal is delivered, after coverage has flushed.
110
+ const timer = setTimeout(() => {}, 2000);
111
+ try {
112
+ process.kill(process.pid, status);
113
+ } catch {
114
+ clearTimeout(timer);
115
+ }
116
+ }
117
+ };
118
+
119
+ const proxySignals = (execution) => {
120
+ const state = {
121
+ finished: false, signal: null
122
+ };
123
+ const listeners = ['SIGINT', 'SIGTERM', 'SIGHUP'].map((signal) => {
124
+ const listener = () => {
125
+ state.signal ||= signal;
126
+ if (!state.finished) {
127
+ execution.kill(signal);
128
+ }
129
+ };
130
+ process.on(signal, listener);
131
+ return [signal, listener];
132
+ });
133
+ state.dispose = () => {
134
+ for (const [signal, listener] of listeners) {
135
+ process.removeListener(signal, listener);
136
+ }
137
+ };
138
+ return state;
139
+ };
140
+
141
+ const logSpawnFailure = (result) => {
142
+ if (result.failed && !Number.isInteger(result.exitCode) && !result.signal) {
143
+ Util.logError(result.shortMessage || result.message);
144
+ }
145
+ };
146
+
147
+ const executeSubprocess = async (command, args, onClose) => {
148
+ const execution = execa(command, args, {
149
+ stdio: 'inherit',
150
+ reject: false
151
+ });
152
+ const signals = proxySignals(execution);
153
+ let status;
154
+ try {
155
+ const result = await execution;
156
+ signals.finished = true;
157
+ logSpawnFailure(result);
158
+ // Preserve the native ChildProcess API exposed to onReady hooks.
159
+ status = await onClose(result.exitCode ?? (result.failed ? 1 : 0), result.signal || signals.signal, execution.nodeChildProcess);
160
+ status = signals.signal || status;
161
+ } finally {
162
+ signals.dispose();
163
+ }
164
+ applyExitStatus(status);
165
+ };
166
+
167
+ const executeCommand = async ([command, ... commandArgs], cliOptions, testArgs = []) => {
168
+
169
+ Util.logInfo(`Execute: ${EC.cyan([command, ... commandArgs].join(' '))}`);
138
170
 
139
171
  if (command === 'merge') {
140
172
  return mergeCoverage(cliOptions);
@@ -145,11 +177,24 @@ const executeCommand = async (command, cliOptions) => {
145
177
 
146
178
  // console.log(options);
147
179
  const coverageReport = MCR(cliOptions);
180
+ const isTest = command === 'test';
148
181
  await coverageReport.loadConfig(cliOptions.config);
149
- coverageReport.cleanCache();
150
182
 
151
183
  const coverageOptions = coverageReport.options;
184
+ let childArgs;
185
+ if (isTest) {
186
+ const targets = getTestTargets(coverageOptions.test);
187
+ if (!targets) {
188
+ return;
189
+ }
190
+ childArgs = ['--test', ... testArgs, ... targets];
191
+ }
192
+ coverageReport.cleanCache();
152
193
  const nodeV8CoverageDir = initNodeV8CoverageDir(coverageOptions);
194
+ if (isTest) {
195
+ // Tests may spawn children with a different working directory.
196
+ process.env.NODE_V8_COVERAGE = path.resolve(nodeV8CoverageDir);
197
+ }
153
198
 
154
199
  // =========================================
155
200
  // onStart hook
@@ -159,31 +204,39 @@ const executeCommand = async (command, cliOptions) => {
159
204
  }
160
205
  // =========================================
161
206
 
162
- const subprocess = foregroundChild(command, {
163
- shell: true
164
- }, async (code, signal) => {
207
+ const onClose = async (code, signal, subprocess) => {
165
208
 
166
209
  // generate coverage even it is failed. code != 0
167
210
 
168
- // =========================================
169
- // onReady hook before adding coverage data.
170
- // Sometimes, the child process has not yet finished writing the coverage data, and it needs to wait here.
171
- const onReady = coverageOptions.onReady;
172
- if (typeof onReady === 'function') {
173
- await onReady(coverageReport, nodeV8CoverageDir, subprocess);
211
+ try {
212
+ // onReady hook before adding coverage data. Some children need
213
+ // additional time to finish writing their coverage files.
214
+ const onReady = coverageOptions.onReady;
215
+ if (typeof onReady === 'function') {
216
+ await onReady(coverageReport, nodeV8CoverageDir, subprocess);
217
+ }
218
+
219
+ await coverageReport.addFromDir(nodeV8CoverageDir);
220
+ const report = await coverageReport.generate();
221
+ if (isTest && !report) {
222
+ throw new Error('No coverage report was generated');
223
+ }
224
+ } catch (error) {
225
+ Util.logError(`Coverage report generation failed: ${error.message}`);
226
+ process.exitCode = 1;
227
+ } finally {
228
+ if (!Util.isDebug()) {
229
+ Util.rmSync(nodeV8CoverageDir);
230
+ }
174
231
  }
175
- // =========================================
176
-
177
- await coverageReport.addFromDir(nodeV8CoverageDir);
178
- await coverageReport.generate();
179
232
 
180
- // remove nodeV8CoverageDir
181
- if (!Util.isDebug()) {
182
- Util.rmSync(nodeV8CoverageDir);
183
- }
233
+ return getExitStatus(code, signal);
234
+ };
184
235
 
185
- return process.exitCode;
186
- });
236
+ if (isTest) {
237
+ return executeSubprocess(process.execPath, childArgs, onClose);
238
+ }
239
+ return executeSubprocess(command, commandArgs, onClose);
187
240
 
188
241
  };
189
242
 
@@ -208,11 +261,35 @@ process.argv.forEach((it) => {
208
261
  }
209
262
  });
210
263
 
264
+ // Also recognize `mcr -c config -- test targets -- runner-options` without
265
+ // falling through to the shell's unrelated `test` command. Only MCR options
266
+ // before the first separator are parsed in that form.
267
+ const getTestCommand = (args, subArgs) => {
268
+ if (args[0] === 'test') {
269
+ return {
270
+ targets: args.slice(1), runnerArgs: subArgs.slice(1)
271
+ };
272
+ }
273
+ if (args.length || subArgs[1] !== 'test') {
274
+ return;
275
+ }
276
+ const remaining = subArgs.slice(2);
277
+ const index = remaining.indexOf('--');
278
+ if (index === -1) {
279
+ return {
280
+ targets: remaining, runnerArgs: []
281
+ };
282
+ }
283
+ return {
284
+ targets: remaining.slice(0, index), runnerArgs: remaining.slice(index + 1)
285
+ };
286
+ };
287
+
211
288
  program
212
289
  .name('mcr')
213
290
  .description('CLI to generate coverage reports')
214
291
  .version(version, '-v, --version', 'output the current version')
215
- .argument('[command]', 'command to execute')
292
+ .argument('[command]', 'command to execute, or built-in merge/test')
216
293
  .allowUnknownOption()
217
294
  .allowExcessArguments()
218
295
  .option('-c, --config <path>', 'custom config file path')
@@ -229,6 +306,7 @@ program
229
306
 
230
307
  .option('--entryFilter <pattern>', 'entry url filter')
231
308
  .option('--sourceFilter <pattern>', 'source path filter')
309
+ .option('--outputFilter <pattern>', 'final source path filter')
232
310
  .option('--filter <pattern>', 'the combined filter')
233
311
 
234
312
  .option('--outputFile <path>', 'output file for v8 report')
@@ -238,22 +316,40 @@ program
238
316
  .option('--lcov', 'generate lcov.info file')
239
317
 
240
318
  .option('--import <module>', 'preload module at startup')
241
- .option('--require <module>', 'preload module at startup')
319
+ .option('--require <module>', 'deprecated alias for --import (ESM resolution)')
242
320
 
243
321
  .option('--env [path]', 'env file (default: ".env")')
244
322
 
245
323
  .action((_command, cliOptions) => {
324
+ const testCommand = getTestCommand(program.args, subArgv);
325
+ if (testCommand) {
326
+ // Commander has already removed MCR options and their values.
327
+ // Positional targets override config; only args after -- are
328
+ // forwarded unchanged to the Node runner.
329
+ const { targets, runnerArgs } = testCommand;
330
+ if (targets.length) {
331
+ cliOptions.test = targets;
332
+ }
333
+ executeCommand(['test'], cliOptions, runnerArgs).catch((error) => {
334
+ Util.logError(error.message);
335
+ process.exitCode = 1;
336
+ });
337
+ return;
338
+ }
339
+
246
340
  const args = [].concat(program.args).concat(subArgv);
247
341
  if (args[0] === '--') {
248
342
  args.shift();
249
343
  }
250
- const command = args.join(' ').trim();
251
- if (!command) {
344
+ if (!args.length || !args[0]) {
252
345
  program.outputHelp();
253
346
  return;
254
347
  }
255
348
 
256
- executeCommand(command, cliOptions);
349
+ executeCommand(args, cliOptions).catch((error) => {
350
+ Util.logError(error.message);
351
+ process.exitCode = 1;
352
+ });
257
353
  });
258
354
 
259
355
  program.parse(argv);
@@ -1,18 +1,19 @@
1
1
 
2
- const EC = require('eight-colors');
2
+ import EC from 'eight-colors';
3
3
 
4
- const { WebSocket } = require('../packages/monocart-coverage-vendor.js');
5
- const Util = require('../utils/util.js');
4
+ import { Util } from '../utils/util.js';
6
5
 
7
- const WSSession = require('./ws-session.js');
8
- const CoverageClient = require('./coverage-client.js');
6
+ import { WSSession } from './ws-session.js';
7
+ import { CoverageClient } from './coverage-client.js';
9
8
 
10
9
  const getDebuggerUrl = async (options) => {
11
10
 
12
11
  const protocol = options.secure ? 'https' : 'http';
13
12
 
14
13
  const url = `${protocol}://${options.host}:${options.port}/json/list`;
15
- const [err, res] = await Util.request(url);
14
+ const [err, res] = await Util.request(url, {
15
+ timeout: options.timeout
16
+ });
16
17
  if (err) {
17
18
  return [err];
18
19
  }
@@ -46,49 +47,78 @@ const getCDPUrl = async (options) => {
46
47
 
47
48
  };
48
49
 
49
- const getCDPSession = async (options) => {
50
-
51
- if (options.session) {
52
- return [null, options.session];
53
- }
54
-
55
- // create session
56
- const [err, url] = await getCDPUrl(options);
57
-
58
- return new Promise((resolve) => {
59
-
60
- if (err) {
61
- resolve([err]);
62
- return;
63
- }
64
-
65
- const timeoutId = setTimeout(() => {
66
- resolve([new Error(`Timeout to connect: ${url}`)]);
67
- }, options.timeout);
68
-
50
+ const connectWebSocket = (WebSocket, url, options) => new Promise((resolve) => {
51
+ let ws;
52
+ try {
69
53
  Util.logDebug(`Connect to ${url}`);
70
- const ws = new WebSocket(url, [], {
54
+ ws = new WebSocket(url, [], {
71
55
  maxPayload: 256 * 1024 * 1024,
72
56
  perMessageDeflate: false,
73
57
  followRedirects: true,
74
58
  ... options.ws
75
59
  });
76
-
77
- ws.once('error', (wsErr) => {
78
- clearTimeout(timeoutId);
79
- resolve([wsErr]);
80
- });
81
-
82
- ws.once('open', () => {
83
- clearTimeout(timeoutId);
84
- Util.logDebug(`${EC.green('Connected')} ${url}`);
85
- const session = new WSSession(ws);
86
- resolve([null, session]);
60
+ } catch (error) {
61
+ resolve([error]);
62
+ return;
63
+ }
64
+ let settled = false;
65
+ let timer;
66
+ const cleanup = () => {
67
+ clearTimeout(timer);
68
+ ws.off('open', onOpen);
69
+ ws.off('error', onError);
70
+ ws.off('close', onClose);
71
+ };
72
+ const fail = (error) => {
73
+ if (settled) {
74
+ return;
75
+ }
76
+ settled = true;
77
+ clearTimeout(timer);
78
+ ws.off('open', onOpen);
79
+ resolve([error]);
80
+ // Keep error/close handlers through termination of a pending handshake.
81
+ if (ws.readyState !== 3) {
82
+ ws.terminate();
83
+ }
84
+ };
85
+ const onError = (error) => fail(error);
86
+ const onClose = () => {
87
+ fail(new Error(`Connection closed before opening: ${url}`));
88
+ cleanup();
89
+ };
90
+ const onOpen = () => {
91
+ const session = new WSSession(ws, {
92
+ timeout: options.requestTimeout
87
93
  });
94
+ settled = true;
95
+ cleanup();
96
+ Util.logDebug(`${EC.green('Connected')} ${url}`);
97
+ resolve([null, session]);
98
+ };
99
+ ws.on('error', onError);
100
+ ws.once('close', onClose);
101
+ ws.once('open', onOpen);
102
+ if (options.timeout > 0) {
103
+ timer = setTimeout(() => fail(new Error(`Timeout to connect: ${url}`)), options.timeout);
104
+ }
105
+ });
88
106
 
89
- });
90
-
91
-
107
+ const getCDPSession = async (options) => {
108
+ if (options.session) {
109
+ return [null, options.session];
110
+ }
111
+ for (const key of ['timeout', 'requestTimeout']) {
112
+ if (!Number.isFinite(options[key]) || options[key] < 0) {
113
+ return [new RangeError(`${key} must be a non-negative finite number`)];
114
+ }
115
+ }
116
+ const [err, url] = await getCDPUrl(options);
117
+ if (err) {
118
+ return [err];
119
+ }
120
+ const { WebSocket } = await import('ws');
121
+ return connectWebSocket(WebSocket, url, options);
92
122
  };
93
123
 
94
124
 
@@ -109,7 +139,8 @@ const CDPClient = async (cdpOptions) => {
109
139
  return targets.find((it) => it.webSocketDebuggerUrl);
110
140
  },
111
141
  ws: {},
112
- timeout: 10 * 1000
142
+ timeout: 10 * 1000,
143
+ requestTimeout: 30 * 1000
113
144
  };
114
145
 
115
146
  if (typeof cdpOptions === 'string') {
@@ -140,4 +171,4 @@ const CDPClient = async (cdpOptions) => {
140
171
 
141
172
  };
142
173
 
143
- module.exports = CDPClient;
174
+ export { CDPClient };
@@ -1,4 +1,4 @@
1
- const Util = require('../utils/util.js');
1
+ import { Util } from '../utils/util.js';
2
2
 
3
3
  const bindEvents = (target, events) => {
4
4
  Object.keys(events).forEach((eventType) => {
@@ -80,6 +80,17 @@ class CoverageClient {
80
80
  this.session = session;
81
81
  }
82
82
 
83
+ // Best-effort background commands return null when a request fails.
84
+ // Keep critical commands on session.send() so their errors still propagate.
85
+ async safeSend(method, params) {
86
+ try {
87
+ return await this.session.send(method, params);
88
+ } catch (error) {
89
+ Util.logDebug(`${method}: ${error?.message || error}`);
90
+ return null;
91
+ }
92
+ }
93
+
83
94
  // =================================================================================================
84
95
  async startJSCoverage() {
85
96
  if (!this.session || this.enabledJS) {
@@ -90,14 +101,16 @@ class CoverageClient {
90
101
  this.jsEvents = {
91
102
  'Debugger.scriptParsed': (params) => {
92
103
  const { scriptId } = params;
93
- this.session.send('Debugger.getScriptSource', {
104
+ this.safeSend('Debugger.getScriptSource', {
94
105
  scriptId
95
106
  }).then((res) => {
96
- this.scriptSources.set(scriptId, res && res.scriptSource);
107
+ if (res) {
108
+ this.scriptSources.set(scriptId, res.scriptSource);
109
+ }
97
110
  });
98
111
  },
99
112
  'Debugger.paused': () => {
100
- this.send('Debugger.resume');
113
+ this.safeSend('Debugger.resume');
101
114
  }
102
115
  };
103
116
 
@@ -169,11 +182,14 @@ class CoverageClient {
169
182
 
170
183
  // anonymous url
171
184
  const url = sourceURL || '';
172
- this.session.send('CSS.getStyleSheetText', {
185
+ this.safeSend('CSS.getStyleSheetText', {
173
186
  styleSheetId
174
187
  }).then((res) => {
188
+ if (!res) {
189
+ return;
190
+ }
175
191
  // add source
176
- const text = res && res.text;
192
+ const text = res.text;
177
193
  if (!text) {
178
194
  Util.logDebug(`Not found css source: ${url}`);
179
195
  }
@@ -272,7 +288,7 @@ class CoverageClient {
272
288
  // write the coverage started by NODE_V8_COVERAGE to disk on demand
273
289
  const res = await this.session.send('Runtime.evaluate', {
274
290
  expression: `new Promise((resolve) => {
275
- require("v8").takeCoverage();
291
+ process.getBuiltinModule("v8").takeCoverage();
276
292
  resolve(process.env.NODE_V8_COVERAGE);
277
293
  })`,
278
294
  includeCommandLineAPI: true,
@@ -319,4 +335,4 @@ class CoverageClient {
319
335
  }
320
336
  }
321
337
 
322
- module.exports = CoverageClient;
338
+ export { CoverageClient };