chrome-devtools-mcp 0.22.0 → 0.24.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 (78) hide show
  1. package/README.md +4 -0
  2. package/build/src/DevToolsConnectionAdapter.js +1 -0
  3. package/build/src/DevtoolsUtils.js +1 -0
  4. package/build/src/HeapSnapshotManager.js +16 -0
  5. package/build/src/McpContext.js +54 -126
  6. package/build/src/McpPage.js +204 -0
  7. package/build/src/McpResponse.js +44 -6
  8. package/build/src/Mutex.js +1 -0
  9. package/build/src/PageCollector.js +1 -0
  10. package/build/src/SlimMcpResponse.js +1 -0
  11. package/build/src/TextSnapshot.js +236 -0
  12. package/build/src/WaitForHelper.js +6 -0
  13. package/build/src/bin/check-latest-version.js +1 -0
  14. package/build/src/bin/chrome-devtools-cli-options.js +206 -46
  15. package/build/src/bin/chrome-devtools-mcp-cli-options.js +13 -1
  16. package/build/src/bin/chrome-devtools-mcp-main.js +1 -0
  17. package/build/src/bin/chrome-devtools-mcp.js +1 -0
  18. package/build/src/bin/chrome-devtools.js +27 -27
  19. package/build/src/browser.js +1 -0
  20. package/build/src/daemon/client.js +14 -12
  21. package/build/src/daemon/daemon.js +7 -5
  22. package/build/src/daemon/types.js +1 -0
  23. package/build/src/daemon/utils.js +20 -14
  24. package/build/src/formatters/ConsoleFormatter.js +48 -1
  25. package/build/src/formatters/HeapSnapshotFormatter.js +18 -2
  26. package/build/src/formatters/IssueFormatter.js +1 -0
  27. package/build/src/formatters/NetworkFormatter.js +1 -0
  28. package/build/src/formatters/SnapshotFormatter.js +2 -1
  29. package/build/src/index.js +114 -51
  30. package/build/src/issue-descriptions.js +1 -0
  31. package/build/src/logger.js +1 -0
  32. package/build/src/polyfill.js +1 -0
  33. package/build/src/telemetry/ClearcutLogger.js +13 -1
  34. package/build/src/telemetry/WatchdogClient.js +1 -0
  35. package/build/src/telemetry/flagUtils.js +1 -0
  36. package/build/src/telemetry/metricUtils.js +1 -0
  37. package/build/src/telemetry/persistence.js +1 -0
  38. package/build/src/telemetry/toolMetricsUtils.js +2 -1
  39. package/build/src/telemetry/types.js +1 -0
  40. package/build/src/telemetry/watchdog/ClearcutSender.js +1 -0
  41. package/build/src/telemetry/watchdog/main.js +1 -0
  42. package/build/src/third_party/THIRD_PARTY_NOTICES +32 -5
  43. package/build/src/third_party/bundled-packages.json +3 -2
  44. package/build/src/third_party/devtools-formatter-worker.js +2451 -2933
  45. package/build/src/third_party/devtools-heap-snapshot-worker.js +32 -26
  46. package/build/src/third_party/index.js +1942 -1536
  47. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +21717 -20261
  48. package/build/src/tools/ToolDefinition.js +1 -0
  49. package/build/src/tools/categories.js +6 -2
  50. package/build/src/tools/console.js +3 -0
  51. package/build/src/tools/emulation.js +2 -0
  52. package/build/src/tools/extensions.js +6 -0
  53. package/build/src/tools/inPage.js +5 -35
  54. package/build/src/tools/input.js +13 -2
  55. package/build/src/tools/lighthouse.js +17 -9
  56. package/build/src/tools/memory.js +34 -1
  57. package/build/src/tools/network.js +7 -2
  58. package/build/src/tools/pages.js +218 -146
  59. package/build/src/tools/performance.js +6 -0
  60. package/build/src/tools/screencast.js +25 -10
  61. package/build/src/tools/screenshot.js +3 -0
  62. package/build/src/tools/script.js +2 -0
  63. package/build/src/tools/slim/tools.js +4 -0
  64. package/build/src/tools/snapshot.js +5 -1
  65. package/build/src/tools/tools.js +1 -0
  66. package/build/src/tools/webmcp.js +3 -0
  67. package/build/src/trace-processing/parse.js +1 -0
  68. package/build/src/types.js +1 -0
  69. package/build/src/utils/check-for-updates.js +1 -0
  70. package/build/src/utils/files.js +5 -10
  71. package/build/src/utils/id.js +1 -0
  72. package/build/src/utils/keyboard.js +1 -0
  73. package/build/src/utils/pagination.js +1 -0
  74. package/build/src/utils/string.js +1 -0
  75. package/build/src/utils/types.js +1 -0
  76. package/build/src/version.js +2 -1
  77. package/package.json +10 -9
  78. package/build/src/bin/cliDefinitions.js +0 -621
@@ -164,6 +164,11 @@ export const cliOptions = {
164
164
  describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.',
165
165
  hidden: true,
166
166
  },
167
+ experimentalNavigationAllowlist: {
168
+ type: 'boolean',
169
+ describe: 'Whether to enable navigation allowlist tool parameter.',
170
+ hidden: true,
171
+ },
167
172
  experimentalInteropTools: {
168
173
  type: 'boolean',
169
174
  describe: 'Whether to enable interoperability tools',
@@ -173,6 +178,11 @@ export const cliOptions = {
173
178
  type: 'boolean',
174
179
  describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
175
180
  },
181
+ experimentalFfmpegPath: {
182
+ type: 'string',
183
+ describe: 'Path to ffmpeg executable for screencast recording.',
184
+ implies: 'experimentalScreencast',
185
+ },
176
186
  experimentalWebmcp: {
177
187
  type: 'boolean',
178
188
  describe: 'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
@@ -206,9 +216,10 @@ export const cliOptions = {
206
216
  default: false,
207
217
  describe: 'Set to true to include tools related to extensions. Note: This feature is currently only supported with a pipe connection. autoConnect, browserUrl, and wsEndpoint are not supported with this feature until 149 will be released.',
208
218
  },
209
- categoryInPageTools: {
219
+ categoryExperimentalInPage: {
210
220
  type: 'boolean',
211
221
  hidden: true,
222
+ default: false,
212
223
  describe: 'Set to true to enable tools exposed by the inspected page itself',
213
224
  },
214
225
  performanceCrux: {
@@ -334,3 +345,4 @@ export function parseArguments(version, argv = process.argv) {
334
345
  .version(version)
335
346
  .parseSync();
336
347
  }
348
+ //# sourceMappingURL=chrome-devtools-mcp-cli-options.js.map
@@ -35,3 +35,4 @@ logger('Chrome DevTools MCP Server connected');
35
35
  logDisclaimers(args);
36
36
  void clearcutLogger?.logDailyActiveIfNeeded();
37
37
  void clearcutLogger?.logServerStart(computeFlagUsage(args, cliOptions));
38
+ //# sourceMappingURL=chrome-devtools-mcp-main.js.map
@@ -20,3 +20,4 @@ if (major < 20) {
20
20
  process.exit(1);
21
21
  }
22
22
  await import('./chrome-devtools-mcp-main.js');
23
+ //# sourceMappingURL=chrome-devtools-mcp.js.map
@@ -15,9 +15,9 @@ import { VERSION } from '../version.js';
15
15
  import { commands } from './chrome-devtools-cli-options.js';
16
16
  import { cliOptions, parseArguments } from './chrome-devtools-mcp-cli-options.js';
17
17
  await checkForUpdates('Run `npm install -g chrome-devtools-mcp@latest` and `chrome-devtools start` to update and restart the daemon.');
18
- async function start(args) {
18
+ async function start(args, sessionId) {
19
19
  const combinedArgs = [...args, ...defaultArgs];
20
- await startDaemon(combinedArgs);
20
+ await startDaemon(combinedArgs, sessionId);
21
21
  logDisclaimers(parseArguments(VERSION, combinedArgs));
22
22
  }
23
23
  const defaultArgs = ['--viaCli', '--experimentalStructuredContent'];
@@ -28,20 +28,10 @@ const startCliOptions = {
28
28
  delete startCliOptions.autoConnect;
29
29
  // Missing CLI serialization.
30
30
  delete startCliOptions.viewport;
31
- // CLI is generated based on the default tool definitions. To enable conditional
32
- // tools, they need to be enabled during CLI generation.
33
- delete startCliOptions.experimentalPageIdRouting;
34
- delete startCliOptions.experimentalVision;
35
- delete startCliOptions.experimentalWebmcp;
36
- delete startCliOptions.experimentalInteropTools;
37
- delete startCliOptions.experimentalScreencast;
38
- delete startCliOptions.categoryEmulation;
39
- delete startCliOptions.categoryPerformance;
40
- delete startCliOptions.categoryNetwork;
41
- delete startCliOptions.categoryExtensions;
42
- // Always on in CLI.
31
+ // Change the defaults for the CLI.
43
32
  delete startCliOptions.experimentalStructuredContent;
44
- // Change the defaults.
33
+ delete startCliOptions.experimentalInteropTools;
34
+ delete startCliOptions.experimentalPageIdRouting;
45
35
  if (!('default' in cliOptions.headless)) {
46
36
  throw new Error('headless cli option unexpectedly does not have a default');
47
37
  }
@@ -51,11 +41,18 @@ if ('default' in cliOptions.isolated) {
51
41
  startCliOptions.headless.default = true;
52
42
  startCliOptions.isolated.description =
53
43
  'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed. Defaults to true unless userDataDir is provided.';
44
+ startCliOptions.categoryExtensions.default = true;
54
45
  const y = yargs(hideBin(process.argv))
55
46
  .scriptName('chrome-devtools')
56
47
  .showHelpOnFail(true)
57
48
  .usage('chrome-devtools <command> [...args] --flags')
58
49
  .usage(`Run 'chrome-devtools <command> --help' for help on the specific command.`)
50
+ .option('sessionId', {
51
+ type: 'string',
52
+ description: 'Session ID for daemon scoping',
53
+ default: '',
54
+ hidden: true,
55
+ })
59
56
  .demandCommand()
60
57
  .version(VERSION)
61
58
  .strict()
@@ -65,8 +62,8 @@ y.command('start', 'Start or restart chrome-devtools-mcp', y => y
65
62
  .options(startCliOptions)
66
63
  .example('$0 start --browserUrl http://localhost:9222', 'Start the server connecting to an existing browser')
67
64
  .strict(), async (argv) => {
68
- if (isDaemonRunning()) {
69
- await stopDaemon();
65
+ if (isDaemonRunning(argv.sessionId)) {
66
+ await stopDaemon(argv.sessionId);
70
67
  }
71
68
  // Defaults but we do not want to affect the yargs conflict resolution.
72
69
  if (argv.isolated === undefined && argv.userDataDir === undefined) {
@@ -76,15 +73,15 @@ y.command('start', 'Start or restart chrome-devtools-mcp', y => y
76
73
  argv.headless = true;
77
74
  }
78
75
  const args = serializeArgs(cliOptions, argv);
79
- await start(args);
76
+ await start(args, argv.sessionId);
80
77
  process.exit(0);
81
78
  }).strict(); // Re-enable strict validation for other commands; this is applied to the yargs instance itself
82
- y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
83
- if (isDaemonRunning()) {
79
+ y.command('status', 'Checks if chrome-devtools-mcp is running', y => y, async (argv) => {
80
+ if (isDaemonRunning(argv.sessionId)) {
84
81
  console.log('chrome-devtools-mcp daemon is running.');
85
82
  const response = await sendCommand({
86
83
  method: 'status',
87
- });
84
+ }, argv.sessionId);
88
85
  if (response.success) {
89
86
  const data = JSON.parse(response.result);
90
87
  console.log(`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`);
@@ -100,11 +97,12 @@ y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
100
97
  }
101
98
  process.exit(0);
102
99
  });
103
- y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
104
- if (!isDaemonRunning()) {
100
+ y.command('stop', 'Stop chrome-devtools-mcp if any', y => y, async (argv) => {
101
+ const sessionId = argv.sessionId;
102
+ if (!isDaemonRunning(sessionId)) {
105
103
  process.exit(0);
106
104
  }
107
- await stopDaemon();
105
+ await stopDaemon(sessionId);
108
106
  process.exit(0);
109
107
  });
110
108
  for (const [commandName, commandDef] of Object.entries(commands)) {
@@ -159,9 +157,10 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
159
157
  }
160
158
  }
161
159
  }, async (argv) => {
160
+ const sessionId = argv.sessionId;
162
161
  try {
163
- if (!isDaemonRunning()) {
164
- await start([]);
162
+ if (!isDaemonRunning(sessionId)) {
163
+ await start([], sessionId);
165
164
  }
166
165
  const commandArgs = {};
167
166
  for (const argName of Object.keys(args)) {
@@ -173,7 +172,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
173
172
  method: 'invoke_tool',
174
173
  tool: commandName,
175
174
  args: commandArgs,
176
- });
175
+ }, sessionId);
177
176
  if (response.success) {
178
177
  console.log(await handleResponse(JSON.parse(response.result), argv['output-format']));
179
178
  }
@@ -189,3 +188,4 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
189
188
  });
190
189
  }
191
190
  await y.parse();
191
+ //# sourceMappingURL=chrome-devtools.js.map
@@ -201,3 +201,4 @@ export async function ensureBrowserLaunched(options) {
201
201
  browser = await launch(options);
202
202
  return browser;
203
203
  }
204
+ //# sourceMappingURL=browser.js.map
@@ -8,7 +8,7 @@ import fs from 'node:fs';
8
8
  import net from 'node:net';
9
9
  import { logger } from '../logger.js';
10
10
  import { PipeTransport } from '../third_party/index.js';
11
- import { saveTemporaryFile } from '../utils/files.js';
11
+ import { getTempFilePath } from '../utils/files.js';
12
12
  import { DAEMON_SCRIPT_PATH, getSocketPath, getPidFilePath, isDaemonRunning, } from './utils.js';
13
13
  const FILE_TIMEOUT = 10_000;
14
14
  /**
@@ -48,12 +48,12 @@ function waitForFile(filePath, removed = false) {
48
48
  });
49
49
  });
50
50
  }
51
- export async function startDaemon(mcpArgs = []) {
52
- if (isDaemonRunning()) {
51
+ export async function startDaemon(mcpArgs = [], sessionId) {
52
+ if (isDaemonRunning(sessionId)) {
53
53
  logger('Daemon is already running');
54
54
  return;
55
55
  }
56
- const pidFilePath = getPidFilePath();
56
+ const pidFilePath = getPidFilePath(sessionId);
57
57
  if (fs.existsSync(pidFilePath)) {
58
58
  fs.unlinkSync(pidFilePath);
59
59
  }
@@ -61,7 +61,7 @@ export async function startDaemon(mcpArgs = []) {
61
61
  const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
62
62
  detached: true,
63
63
  stdio: 'ignore',
64
- env: process.env,
64
+ env: { ...process.env, CHROME_DEVTOOLS_MCP_SESSION_ID: sessionId },
65
65
  cwd: process.cwd(),
66
66
  windowsHide: true,
67
67
  });
@@ -72,8 +72,8 @@ const SEND_COMMAND_TIMEOUT = 60_000; // ms
72
72
  /**
73
73
  * `sendCommand` opens a socket connection sends a single command and disconnects.
74
74
  */
75
- export async function sendCommand(command) {
76
- const socketPath = getSocketPath();
75
+ export async function sendCommand(command, sessionId) {
76
+ const socketPath = getSocketPath(sessionId);
77
77
  const socket = net.createConnection({
78
78
  path: socketPath,
79
79
  });
@@ -102,13 +102,13 @@ export async function sendCommand(command) {
102
102
  transport.send(JSON.stringify(command));
103
103
  });
104
104
  }
105
- export async function stopDaemon() {
106
- if (!isDaemonRunning()) {
105
+ export async function stopDaemon(sessionId) {
106
+ if (!isDaemonRunning(sessionId)) {
107
107
  logger('Daemon is not running');
108
108
  return;
109
109
  }
110
- const pidFilePath = getPidFilePath();
111
- await sendCommand({ method: 'stop' });
110
+ const pidFilePath = getPidFilePath(sessionId);
111
+ await sendCommand({ method: 'stop' }, sessionId);
112
112
  await waitForFile(pidFilePath, /*removed=*/ true);
113
113
  }
114
114
  export async function handleResponse(response, format) {
@@ -141,7 +141,8 @@ export async function handleResponse(response, format) {
141
141
  }
142
142
  const data = Buffer.from(imageData, 'base64');
143
143
  const name = crypto.randomUUID();
144
- const { filepath } = await saveTemporaryFile(data, `${name}${extension}`);
144
+ const filepath = await getTempFilePath(`${name}${extension}`);
145
+ fs.writeFileSync(filepath, data);
145
146
  chunks.push(`Saved to ${filepath}.`);
146
147
  }
147
148
  else {
@@ -150,3 +151,4 @@ export async function handleResponse(response, format) {
150
151
  }
151
152
  return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
152
153
  }
154
+ //# sourceMappingURL=client.js.map
@@ -11,19 +11,20 @@ import process from 'node:process';
11
11
  import { logger } from '../logger.js';
12
12
  import { Client, PipeTransport, StdioClientTransport, } from '../third_party/index.js';
13
13
  import { VERSION } from '../version.js';
14
- import { DAEMON_CLIENT_NAME, getDaemonPid, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
15
- const pid = getDaemonPid();
16
- if (isDaemonRunning(pid)) {
14
+ import { DAEMON_CLIENT_NAME, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
15
+ const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
16
+ logger(`Daemon sessionId: ${sessionId}`);
17
+ if (isDaemonRunning(sessionId)) {
17
18
  logger('Another daemon process is running.');
18
19
  process.exit(1);
19
20
  }
20
- const pidFilePath = getPidFilePath();
21
+ const pidFilePath = getPidFilePath(sessionId);
21
22
  fs.mkdirSync(path.dirname(pidFilePath), {
22
23
  recursive: true,
23
24
  });
24
25
  fs.writeFileSync(pidFilePath, process.pid.toString());
25
26
  logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
26
- const socketPath = getSocketPath();
27
+ const socketPath = getSocketPath(sessionId);
27
28
  const startDate = new Date();
28
29
  const mcpServerArgs = process.argv.slice(2);
29
30
  let mcpClient = null;
@@ -200,3 +201,4 @@ const started = startSocketServer().catch(error => {
200
201
  logger('Failed to start daemon server:', error);
201
202
  process.exit(1);
202
203
  });
204
+ //# sourceMappingURL=daemon.js.map
@@ -4,3 +4,4 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  export {};
7
+ //# sourceMappingURL=types.js.map
@@ -13,46 +13,50 @@ export const INDEX_SCRIPT_PATH = path.join(import.meta.dirname, '..', 'bin', 'ch
13
13
  const APP_NAME = 'chrome-devtools-mcp';
14
14
  export const DAEMON_CLIENT_NAME = 'chrome-devtools-cli-daemon';
15
15
  // Using these paths due to strict limits on the POSIX socket path length.
16
- export function getSocketPath() {
16
+ export function getSocketPath(sessionId) {
17
17
  const uid = os.userInfo().uid;
18
+ const suffix = sessionId ? `-${sessionId}` : '';
19
+ const appName = APP_NAME + suffix;
18
20
  if (IS_WINDOWS) {
19
21
  // Windows uses Named Pipes, not file paths.
20
22
  // This format is required for server.listen()
21
- return path.join('\\\\.\\pipe', APP_NAME, 'server.sock');
23
+ return path.join('\\\\.\\pipe', appName, 'server.sock');
22
24
  }
23
25
  // 1. Try XDG_RUNTIME_DIR (Linux standard, sometimes macOS)
24
26
  if (process.env.XDG_RUNTIME_DIR) {
25
- return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME, 'server.sock');
27
+ return path.join(process.env.XDG_RUNTIME_DIR, appName, 'server.sock');
26
28
  }
27
29
  // 2. macOS/Unix Fallback: Use /tmp/
28
30
  // We use /tmp/ because it is much shorter than ~/Library/Application Support/
29
31
  // and keeps us well under the 104-character limit.
30
- return path.join('/tmp', `${APP_NAME}-${uid}.sock`);
32
+ return path.join('/tmp', `${appName}-${uid}.sock`);
31
33
  }
32
- export function getRuntimeHome() {
34
+ export function getRuntimeHome(sessionId) {
33
35
  const platform = os.platform();
34
36
  const uid = os.userInfo().uid;
37
+ const suffix = sessionId ? `-${sessionId}` : '';
38
+ const appName = APP_NAME + suffix;
35
39
  // 1. Check for the modern Unix standard
36
40
  if (process.env.XDG_RUNTIME_DIR) {
37
- return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME);
41
+ return path.join(process.env.XDG_RUNTIME_DIR, appName);
38
42
  }
39
43
  // 2. Fallback for macOS and older Linux
40
44
  if (platform === 'darwin' || platform === 'linux') {
41
45
  // /tmp is cleared on boot, making it perfect for PIDs
42
- return path.join('/tmp', `${APP_NAME}-${uid}`);
46
+ return path.join('/tmp', `${appName}-${uid}`);
43
47
  }
44
48
  // 3. Windows Fallback
45
- return path.join(os.tmpdir(), APP_NAME);
49
+ return path.join(os.tmpdir(), appName);
46
50
  }
47
51
  export const IS_WINDOWS = os.platform() === 'win32';
48
- export function getPidFilePath() {
49
- const runtimeDir = getRuntimeHome();
52
+ export function getPidFilePath(sessionId) {
53
+ const runtimeDir = getRuntimeHome(sessionId);
50
54
  return path.join(runtimeDir, 'daemon.pid');
51
55
  }
52
- export function getDaemonPid() {
56
+ export function getDaemonPid(sessionId) {
53
57
  try {
54
- const pidFile = getPidFilePath();
55
- logger(`Daemon pid file ${pidFile}`);
58
+ const pidFile = getPidFilePath(sessionId);
59
+ logger(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
56
60
  if (!fs.existsSync(pidFile)) {
57
61
  return null;
58
62
  }
@@ -68,7 +72,8 @@ export function getDaemonPid() {
68
72
  return null;
69
73
  }
70
74
  }
71
- export function isDaemonRunning(pid = getDaemonPid()) {
75
+ export function isDaemonRunning(sessionId) {
76
+ const pid = getDaemonPid(sessionId);
72
77
  if (pid) {
73
78
  try {
74
79
  process.kill(pid, 0); // Throws if process doesn't exist
@@ -107,3 +112,4 @@ export function serializeArgs(options, argv) {
107
112
  }
108
113
  return args;
109
114
  }
115
+ //# sourceMappingURL=utils.js.map
@@ -131,6 +131,36 @@ export class ConsoleFormatter {
131
131
  id: this.#id,
132
132
  };
133
133
  }
134
+ /**
135
+ * Groups consecutive messages with the same type, text, and argument count.
136
+ * Similar to Chrome DevTools' console grouping behavior.
137
+ */
138
+ static groupConsecutive(messages) {
139
+ const grouped = [];
140
+ for (const msg of messages) {
141
+ const prev = grouped[grouped.length - 1];
142
+ if (prev &&
143
+ prev.message instanceof ConsoleFormatter &&
144
+ msg instanceof ConsoleFormatter &&
145
+ prev.message.#type === msg.#type &&
146
+ prev.message.#text === msg.#text &&
147
+ prev.message.#argCount === msg.#argCount) {
148
+ prev.count++;
149
+ }
150
+ else {
151
+ grouped.push({ message: msg, count: 1 });
152
+ }
153
+ }
154
+ return grouped.map(({ message, count }) => count > 1 && message instanceof ConsoleFormatter
155
+ ? new GroupedConsoleFormatter({
156
+ id: message.#id,
157
+ type: message.#type,
158
+ text: message.#text,
159
+ argCount: message.#argCount,
160
+ isIgnored: message.isIgnored,
161
+ }, count)
162
+ : message);
163
+ }
134
164
  toJSONDetailed() {
135
165
  return {
136
166
  id: this.#id,
@@ -144,8 +174,24 @@ export class ConsoleFormatter {
144
174
  };
145
175
  }
146
176
  }
177
+ export class GroupedConsoleFormatter extends ConsoleFormatter {
178
+ #count;
179
+ constructor(params, count) {
180
+ super(params);
181
+ this.#count = count;
182
+ }
183
+ toString() {
184
+ return convertConsoleMessageConciseToString(this.toJSON());
185
+ }
186
+ toJSON() {
187
+ const json = super.toJSON();
188
+ json.count = this.#count;
189
+ return json;
190
+ }
191
+ }
147
192
  function convertConsoleMessageConciseToString(msg) {
148
- return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)`;
193
+ const countSuffix = msg.count && msg.count > 1 ? ` [${msg.count} times]` : '';
194
+ return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)${countSuffix}`;
149
195
  }
150
196
  function convertConsoleMessageConciseDetailedToString(msg) {
151
197
  const result = [
@@ -239,3 +285,4 @@ function formatCause(cause, formatter) {
239
285
  ...formatStackTraceInner(cause.stackTrace, cause.cause, formatter),
240
286
  ];
241
287
  }
288
+ //# sourceMappingURL=ConsoleFormatter.js.map
@@ -4,13 +4,28 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { stableIdSymbol } from '../utils/id.js';
7
+ export function isNodeLike(item) {
8
+ return (typeof item === 'object' && item !== null && 'id' in item && 'name' in item);
9
+ }
7
10
  export class HeapSnapshotFormatter {
8
11
  #aggregates;
9
12
  constructor(aggregates) {
10
13
  this.#aggregates = aggregates;
11
14
  }
15
+ static formatNodes(items) {
16
+ const lines = [];
17
+ if (items.length > 0 && isNodeLike(items[0])) {
18
+ lines.push('id,name,type,distance,selfSize,retainedSize');
19
+ }
20
+ for (const item of items) {
21
+ if (isNodeLike(item)) {
22
+ lines.push(`${item.id},"${item.name}",${item.type},${item.distance},${item.selfSize},${item.retainedSize}`);
23
+ }
24
+ }
25
+ return lines.join('\n');
26
+ }
12
27
  #getSortedAggregates() {
13
- return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
28
+ return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
14
29
  }
15
30
  toString() {
16
31
  const sorted = this.#getSortedAggregates();
@@ -33,6 +48,7 @@ export class HeapSnapshotFormatter {
33
48
  }));
34
49
  }
35
50
  static sort(aggregates) {
36
- return Object.entries(aggregates).sort((a, b) => b[1].self - a[1].self);
51
+ return Object.entries(aggregates).sort((a, b) => b[1].maxRet - a[1].maxRet);
37
52
  }
38
53
  }
54
+ //# sourceMappingURL=HeapSnapshotFormatter.js.map
@@ -190,3 +190,4 @@ function convertIssueDetailedToString(issue) {
190
190
  result.push(`Message: issue> ${bodyParts.join('\n')}`);
191
191
  return result.join('\n');
192
192
  }
193
+ //# sourceMappingURL=IssueFormatter.js.map
@@ -233,3 +233,4 @@ function converNetworkRequestDetailedToStringDetailed(data) {
233
233
  }
234
234
  return response.join('\n');
235
235
  }
236
+ //# sourceMappingURL=NetworkFormatter.js.map
@@ -12,7 +12,7 @@ export class SnapshotFormatter {
12
12
  const chunks = [];
13
13
  const root = this.#snapshot.root;
14
14
  // Top-level content of the snapshot.
15
- if (this.#snapshot.verbose &&
15
+ if (!this.#snapshot.verbose &&
16
16
  this.#snapshot.hasSelectedElement &&
17
17
  !this.#snapshot.selectedElementUid) {
18
18
  chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
@@ -132,3 +132,4 @@ const excludedAttributes = new Set([
132
132
  'backendNodeId',
133
133
  'loaderId',
134
134
  ]);
135
+ //# sourceMappingURL=SnapshotFormatter.js.map