packages/react-devtools-cdt-mcp/e2e/run.flow.js JAVASCRIPT 1,354 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow strict-local8 */910'use strict';1112const assert = require('assert');13const childProcess = require('child_process');14const fs = require('fs');15const http = require('http');16const net = require('net');17const os = require('os');18const path = require('path');1920// eslint-disable-next-line no-undef21type ChildProcess = child_process$ChildProcess;22type JSONSchemaObject = {23  type?: string,24  properties?: {[string]: JSONSchemaObject, ...},25  required?: Array<string>,26  ...27};28type CommandResult = {29  stdout: string,30  stderr: string,31};32type SpawnOptions = {33  cwd: string,34  detached?: boolean,35  env: {[string]: string | void},36  logFile: string,37};38type CommandOptions = {39  cwd: string,40  env: {[string]: string | void},41  logFile: string,42  timeout?: number,43};44type Chrome = {45  run: (args: Array<string>) => Promise<CommandResult>,46  json: (args: Array<string>) => Promise<mixed>,47};48type TreeNode = {49  uid: string,50  type: string,51  name: string,52  key?: string | null,53  firstChild?: string | null,54  nextSibling?: string | null,55};56type SnapshotNode = {57  id?: string,58  role?: string,59  name?: string,60  children?: Array<SnapshotNode>,61};62type ToolDefinition = {63  name: string,64  description: string,65  inputSchema: JSONSchemaObject,66};67type ToolGroup = {68  name: string,69  description: string,70  tools: Array<ToolDefinition>,71};72type ToolDiscovery = {73  thirdPartyDeveloperTools?: ToolGroup | Array<ToolGroup>,74  ...75};76type SourceResult = {77  source: null | {78    name: string,79    fileName: string,80    line: number,81    column: number,82    ...83  },84  ...85};86type PageReadiness = {87  hasApp: boolean,88  hasHook: boolean,89};90type ComponentDetails = {91  name: string,92  type: string,93  hooks: Array<{name: string, ...}>,94  ...95};96type SearchResult = {97  page: number,98  pageSize: number,99  totalCount: number,100  totalPages: number,101  results: Array<{name: string, ...}>,102  ...103};104type DomLookupResult = {105  type: string,106  name: string,107  ...108};109type OwnersStackResult = {110  stack: string,111  ...112};113type ComponentBranchEntry = {114  uid: string,115  name: string,116  type: string,117  ...118};119type ErrorPayload = {120  error: string,121  ...122};123type StartProfilingResult = {124  status: string,125  traceName: string,126  ...127};128type StopProfilingResult = {129  status: string,130  traceName: string,131  commits: number,132  ...133};134type TraceOverviewCommit = {135  commit: number,136  componentsChanged: number,137  ...138};139type CommitReport = {140  components: Array<{name: string, ...}>,141  ...142};143144const TOOL_NAMES = [145  'react_get_component_tree',146  'react_get_component_by_uid',147  'react_get_component_by_dom_element',148  'react_find_components',149  'react_get_component_source',150  'react_get_owner_stack_trace',151  'react_get_parent_stack',152  'react_get_owner_stack',153  'react_start_profiling',154  'react_stop_profiling',155  'react_get_trace_overview',156  'react_get_commit_report',157];158159const PACKAGE_DIR = path.resolve(__dirname, '..');160const REPO_ROOT = path.resolve(PACKAGE_DIR, '..', '..');161const FIXTURE_DIR = path.join(PACKAGE_DIR, 'fixtures', 'app');162const BUILT_MODULES_DIR = path.join(REPO_ROOT, 'build', 'oss-experimental');163const LOG_DIR =164  process.env.E2E_LOG_DIR ||165  path.join(REPO_ROOT, 'tmp', 'react-devtools-cdt-mcp-e2e');166167const SESSION_ID = `react-devtools-cdt-mcp-${process.pid}-${Date.now()}`;168169function log(message: string): void {170  process.stdout.write(`${message}\n`);171}172173function sleep(ms: number): Promise<void> {174  return new Promise(resolve => setTimeout(resolve, ms));175}176177function createError(message: string): Error {178  // eslint-disable-next-line react-internal/prod-error-codes179  return new Error(message);180}181182function ensureBuiltModules(): void {183  if (!fs.existsSync(BUILT_MODULES_DIR)) {184    throw createError(185      'Missing build/oss-experimental. Run `yarn build-for-devtools` from ' +186        'the repo root before running react-devtools-cdt-mcp E2E tests.'187    );188  }189}190191function getFreePort(): Promise<number> {192  return new Promise((resolve, reject) => {193    const server = net.createServer();194    server.unref();195    server.on('error', reject);196    server.listen(0, '127.0.0.1', undefined, () => {197      const address = server.address();198      if (address == null || typeof address === 'string') {199        reject(createError('Failed to allocate a TCP port'));200        return;201      }202      server.close(() => resolve(address.port));203    });204  });205}206207function appendLog(logFile: string, text: string | Buffer): void {208  fs.appendFileSync(logFile, text);209}210211function formatCommand(command: string, args: Array<string>): string {212  return `$ ${[command, ...args].map(arg => JSON.stringify(arg)).join(' ')}\n`;213}214215function spawnLogged(216  command: string,217  args: Array<string>,218  options: SpawnOptions219): ChildProcess {220  const child = childProcess.spawn(command, args, {221    cwd: options.cwd,222    detached: options.detached === true,223    env: options.env,224    stdio: ['ignore', 'pipe', 'pipe'],225  });226  appendLog(options.logFile, formatCommand(command, args));227  child.on('error', error => {228    appendLog(229      options.logFile,230      `Failed to spawn ${command}: ${231        error.stack || error.message || String(error)232      }\n`233    );234  });235  child.stdout.on('data', chunk => appendLog(options.logFile, chunk));236  child.stderr.on('data', chunk => appendLog(options.logFile, chunk));237  return child;238}239240function waitForExit(child: ChildProcess, timeout: number): Promise<void> {241  return new Promise(resolve => {242    let done = false;243    const finish = () => {244      if (done) {245        return;246      }247      done = true;248      clearTimeout(timer);249      resolve();250    };251    const timer = setTimeout(() => {252      if (child.exitCode == null) {253        child.kill('SIGKILL');254      }255      finish();256    }, timeout);257258    if (child.exitCode != null) {259      finish();260      return;261    }262    child.once('exit', finish);263    child.once('error', finish);264  });265}266267async function removePathWithRetries(268  targetPath: string,269  logFile: string270): Promise<void> {271  for (let attempt = 0; attempt < 5; attempt++) {272    try {273      fs.rmSync(targetPath, {force: true, recursive: true});274      return;275    } catch (error) {276      if (attempt === 4) {277        appendLog(278          logFile,279          `Failed to remove ${targetPath}: ${280            error.stack || error.message || String(error)281          }\n`282        );283        return;284      }285      await sleep(250);286    }287  }288}289290function runCommand(291  command: string,292  args: Array<string>,293  options: CommandOptions294): Promise<CommandResult> {295  const timeout = options.timeout || 120000;296  const logFile = options.logFile;297  return new Promise((resolve, reject) => {298    let stdout = '';299    let stderr = '';300    appendLog(logFile, formatCommand(command, args));301    const child = childProcess.spawn(command, args, {302      cwd: options.cwd,303      env: options.env,304      stdio: ['ignore', 'pipe', 'pipe'],305    });306    const timer = setTimeout(() => {307      child.kill('SIGTERM');308      reject(createError(`Command timed out after ${timeout}ms: ${command}`));309    }, timeout);310311    child.stdout.on('data', chunk => {312      stdout += chunk.toString();313      appendLog(logFile, chunk);314    });315    child.stderr.on('data', chunk => {316      stderr += chunk.toString();317      appendLog(logFile, chunk);318    });319    child.on('error', error => {320      clearTimeout(timer);321      reject(error);322    });323    child.on('close', code => {324      clearTimeout(timer);325      if (code === 0) {326        resolve({stdout, stderr});327      } else {328        reject(329          createError(330            `Command failed with exit code ${code}: ${command} ${args.join(331              ' '332            )}\n${stderr || stdout}`333          )334        );335      }336    });337  });338}339340function startDebuggableChrome(341  chromeExecutablePath: string,342  remoteDebuggingPort: number,343  logFile: string344): {profileDir: string, process: ChildProcess} {345  const profileDir = fs.mkdtempSync(346    path.join(os.tmpdir(), 'react-devtools-cdt-mcp-chrome-')347  );348  appendLog(logFile, `chromeUserDataDir=${profileDir}\n`);349350  const args = [351    '--headless=new',352    `--remote-debugging-port=${remoteDebuggingPort}`,353    '--remote-debugging-address=127.0.0.1',354    `--user-data-dir=${profileDir}`,355    '--no-first-run',356    '--no-default-browser-check',357    'about:blank',358  ];359360  if (process.platform === 'linux') {361    args.splice(1, 0, '--no-sandbox', '--disable-setuid-sandbox');362  }363364  return {365    profileDir,366    process: spawnLogged(chromeExecutablePath, args, {367      cwd: PACKAGE_DIR,368      env: process.env,369      logFile,370    }),371  };372}373374function getChromeDevToolsBin(): string {375  let packageJsonPath: string;376  try {377    packageJsonPath = require.resolve('chrome-devtools-mcp/package.json', {378      paths: [PACKAGE_DIR],379    });380  } catch (error) {381    throw createError(382      'Missing chrome-devtools-mcp dependency. Run `yarn install` before ' +383        'running react-devtools-cdt-mcp E2E tests.'384    );385  }386387  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));388  if (389    packageJson == null ||390    typeof packageJson !== 'object' ||391    packageJson.bin == null ||392    typeof packageJson.bin !== 'object' ||393    typeof packageJson.bin['chrome-devtools'] !== 'string'394  ) {395    throw createError(396      'chrome-devtools-mcp package.json is missing the chrome-devtools bin'397    );398  }399  return path.resolve(400    path.dirname(packageJsonPath),401    packageJson.bin['chrome-devtools']402  );403}404405async function waitForHttp(url: string, timeout: number): Promise<void> {406  const deadline = Date.now() + timeout;407  let lastError: Error = createError('No response yet');408  while (Date.now() < deadline) {409    try {410      const statusCode: number = await new Promise((resolve, reject) => {411        // eslint-disable-next-line no-undef412        const request = http.get(url, (response: http$IncomingMessage<>) => {413          response.resume();414          response.on('end', () => resolve(response.statusCode || 0));415        });416        request.on('error', reject);417        request.setTimeout(1000, () => {418          request.destroy(createError('HTTP request timed out'));419        });420      });421      if (statusCode >= 200 && statusCode < 400) {422        return;423      }424      lastError = createError(`HTTP ${statusCode}`);425    } catch (error) {426      lastError = error;427    }428    await sleep(250);429  }430  throw createError(`Timed out waiting for ${url}: ${lastError.message}`);431}432433async function waitForPageReady(434  chrome: Chrome,435  timeout: number436): Promise<void> {437  const deadline = Date.now() + timeout;438  let lastResult: mixed = null;439  while (Date.now() < deadline) {440    const result = await evaluatePageReadiness(441      chrome,442      `() => ({443      hasApp: document.querySelector('main.app') !== null,444      hasHook: window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null,445      hasDiscoveryListener: window.__dtmcp != null ||446        window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null,447    })`448    );449    lastResult = result;450    if (result.hasApp === true && result.hasHook === true) {451      return;452    }453    await sleep(250);454  }455  throw createError(456    `Timed out waiting for fixture readiness: ${457      JSON.stringify(lastResult) || String(lastResult)458    }`459  );460}461462function parseJsonOutput(stdout: string): mixed {463  const text = stdout.trim();464  assert.notStrictEqual(text, '', 'Expected command to print JSON');465  const parsed = JSON.parse(text);466  if (467    Array.isArray(parsed) &&468    parsed.length === 1 &&469    parsed[0] &&470    parsed[0].type === 'text' &&471    typeof parsed[0].text === 'string'472  ) {473    throw createError(parsed[0].text);474  }475  return parsed;476}477478function parseJsonFromText(text: string): mixed {479  const trimmed = text.trim();480  const fenced = trimmed.match(/```json\n([\s\S]*?)\n```/);481  if (fenced) {482    return JSON.parse(fenced[1]);483  }484  return JSON.parse(trimmed);485}486487function unwrapTextResponse(response: mixed): string {488  if (Array.isArray(response)) {489    return response.join('\n');490  }491  if (typeof response === 'string') {492    return response;493  }494  if (response != null && typeof response === 'object') {495    const message = response.message;496    if (typeof message === 'string') {497      return message;498    }499  }500  return JSON.stringify(response) || String(response);501}502503function formatValue(value: mixed): string {504  return JSON.stringify(value) || String(value);505}506507function expectObject(value: mixed, message: string): {+[string]: mixed, ...} {508  if (value == null || typeof value !== 'object' || Array.isArray(value)) {509    throw createError(`${message}. Saw: ${formatValue(value)}`);510  }511  return value;512}513514function expectString(value: mixed, message: string): string {515  if (typeof value !== 'string') {516    throw createError(`${message}. Saw: ${formatValue(value)}`);517  }518  return value;519}520521function expectNumber(value: mixed, message: string): number {522  if (typeof value !== 'number') {523    throw createError(`${message}. Saw: ${formatValue(value)}`);524  }525  return value;526}527528function expectArray(value: mixed, message: string): $ReadOnlyArray<mixed> {529  if (!Array.isArray(value)) {530    throw createError(`${message}. Saw: ${formatValue(value)}`);531  }532  return value;533}534535function expectOptionalString(value: mixed, message: string): string | null {536  if (value == null) {537    return null;538  }539  return expectString(value, message);540}541542function parseJSONSchemaObject(543  value: mixed,544  message: string545): JSONSchemaObject {546  const object = expectObject(value, message);547  const schema: JSONSchemaObject = {};548  if (object.type != null) {549    schema.type = expectString(object.type, `${message}.type must be a string`);550  }551  if (object.properties != null) {552    const rawProperties = expectObject(553      object.properties,554      `${message}.properties must be an object`555    );556    const properties: {[string]: JSONSchemaObject, ...} = {};557    // eslint-disable-next-line no-for-of-loops/no-for-of-loops558    for (const key of Object.keys(rawProperties)) {559      properties[key] = parseJSONSchemaObject(560        rawProperties[key],561        `${message}.properties.${key}`562      );563    }564    schema.properties = properties;565  }566  if (object.required != null) {567    schema.required = expectArray(568      object.required,569      `${message}.required must be an array`570    ).map((item, index) =>571      expectString(item, `${message}.required[${index}] must be a string`)572    );573  }574  return schema;575}576577function parseToolDefinition(value: mixed): ToolDefinition {578  const object = expectObject(value, 'Expected tool definition object');579  return {580    name: expectString(object.name, 'Expected tool definition name'),581    description: expectString(582      object.description,583      'Expected tool definition description'584    ),585    inputSchema: parseJSONSchemaObject(586      object.inputSchema,587      'Expected tool definition inputSchema'588    ),589  };590}591592function parseToolGroup(value: mixed): ToolGroup {593  const object = expectObject(value, 'Expected tool group object');594  return {595    name: expectString(object.name, 'Expected tool group name'),596    description: expectString(597      object.description,598      'Expected tool group description'599    ),600    tools: expectArray(object.tools, 'Expected tool group tools').map(601      parseToolDefinition602    ),603  };604}605606function parseToolDiscovery(value: mixed): ToolDiscovery {607  const object = expectObject(value, 'Expected tool discovery object');608  const rawToolGroups = object.thirdPartyDeveloperTools;609  if (rawToolGroups == null) {610    return {};611  }612  return {613    thirdPartyDeveloperTools: Array.isArray(rawToolGroups)614      ? rawToolGroups.map(parseToolGroup)615      : parseToolGroup(rawToolGroups),616  };617}618619function parsePageReadiness(value: mixed): PageReadiness {620  const object = expectObject(value, 'Expected page readiness object');621  return {622    hasApp: object.hasApp === true,623    hasHook: object.hasHook === true,624  };625}626627async function evaluatePageReadiness(628  chrome: Chrome,629  fn: string630): Promise<PageReadiness> {631  const output = await chrome.json(['evaluate_script', fn]);632  return parsePageReadiness(parseJsonFromText(unwrapTextResponse(output)));633}634635function parseToolResponse(output: mixed): mixed {636  return parseJsonFromText(unwrapTextResponse(output));637}638639function parseTreeNode(value: mixed): TreeNode {640  const object = expectObject(value, 'Expected tree node object');641  return {642    uid: expectString(object.uid, 'Expected tree node uid'),643    type: expectString(object.type, 'Expected tree node type'),644    name: expectString(object.name, 'Expected tree node name'),645    key: expectOptionalString(object.key, 'Expected tree node key'),646    firstChild: expectOptionalString(647      object.firstChild,648      'Expected tree node firstChild'649    ),650    nextSibling: expectOptionalString(651      object.nextSibling,652      'Expected tree node nextSibling'653    ),654  };655}656657function parseTree(value: mixed): Array<TreeNode> {658  const object = expectObject(value, 'Expected component tree response object');659  return expectArray(object.nodes, 'Expected component tree nodes array').map(660    parseTreeNode661  );662}663664function parseNamedObject(value: mixed, message: string): {name: string, ...} {665  const object = expectObject(value, message);666  return {667    name: expectString(object.name, `${message} name`),668  };669}670671function parseComponentBranchEntry(672  value: mixed,673  message: string674): ComponentBranchEntry {675  const object = expectObject(value, message);676  return {677    uid: expectString(object.uid, `${message} uid`),678    name: expectString(object.name, `${message} name`),679    type: expectString(object.type, `${message} type`),680  };681}682683function parseComponentDetails(value: mixed): ComponentDetails {684  const object = expectObject(value, 'Expected component details object');685  const details: ComponentDetails = {686    name: expectString(object.name, 'Expected component details name'),687    type: expectString(object.type, 'Expected component details type'),688    hooks: expectArray(object.hooks, 'Expected component details hooks').map(689      (hook, index) =>690        parseNamedObject(hook, `Expected component hook ${index}`)691    ),692  };693  return details;694}695696function parseComponentType(value: mixed): string {697  return expectString(698    expectObject(value, 'Expected component details object').type,699    'Expected component details type'700  );701}702703function parseSearchResult(value: mixed): SearchResult {704  const object = expectObject(value, 'Expected search result object');705  return {706    ...object,707    page: expectNumber(object.page, 'Expected search result page'),708    pageSize: expectNumber(object.pageSize, 'Expected search result pageSize'),709    totalCount: expectNumber(710      object.totalCount,711      'Expected search result totalCount'712    ),713    totalPages: expectNumber(714      object.totalPages,715      'Expected search result totalPages'716    ),717    results: expectArray(object.results, 'Expected search result results').map(718      (result, index) =>719        parseNamedObject(result, `Expected search result ${index}`)720    ),721  };722}723724function parseSnapshotNode(value: mixed): SnapshotNode {725  const object = expectObject(value, 'Expected snapshot node object');726  const snapshotNode: SnapshotNode = {};727  if (object.id != null) {728    snapshotNode.id = expectString(object.id, 'Expected snapshot node id');729  }730  if (object.role != null) {731    snapshotNode.role = expectString(732      object.role,733      'Expected snapshot node role'734    );735  }736  if (object.name != null) {737    snapshotNode.name = expectString(738      object.name,739      'Expected snapshot node name'740    );741  }742  if (object.children != null) {743    snapshotNode.children = expectArray(744      object.children,745      'Expected snapshot node children'746    ).map(parseSnapshotNode);747  }748  return snapshotNode;749}750751function parseSnapshotResponse(value: mixed): {snapshot: SnapshotNode, ...} {752  const object = expectObject(value, 'Expected snapshot response object');753  return {754    snapshot: parseSnapshotNode(object.snapshot),755  };756}757758function parseDomLookupResult(value: mixed): DomLookupResult {759  const object = expectObject(value, 'Expected DOM lookup result object');760  return {761    ...object,762    type: expectString(object.type, 'Expected DOM lookup result type'),763    name: expectString(object.name, 'Expected DOM lookup result name'),764  };765}766767function parseSourceResult(value: mixed): SourceResult {768  const object = expectObject(value, 'Expected source result object');769  if (object.source == null) {770    return {source: null};771  }772  const source = expectObject(object.source, 'Expected source object');773  return {774    ...object,775    source: {776      ...source,777      name: expectString(source.name, 'Expected source name'),778      fileName: expectString(source.fileName, 'Expected source fileName'),779      line: expectNumber(source.line, 'Expected source line'),780      column: expectNumber(source.column, 'Expected source column'),781    },782  };783}784785function parseOwnersStack(value: mixed): OwnersStackResult {786  const object = expectObject(value, 'Expected owners stack object');787  return {788    ...object,789    stack: expectString(object.stack, 'Expected owners stack string'),790  };791}792793function parseComponentBranch(794  value: mixed,795  label: string796): Array<ComponentBranchEntry> {797  return expectArray(value, `Expected ${label} branch array`).map(798    (entry, index) =>799      parseComponentBranchEntry(entry, `Expected ${label} ${index}`)800  );801}802803function parseErrorPayload(value: mixed): ErrorPayload {804  const object = expectObject(value, 'Expected error payload object');805  return {806    ...object,807    error: expectString(object.error, 'Expected error payload error'),808  };809}810811function parseStartProfilingResult(value: mixed): StartProfilingResult {812  const object = expectObject(value, 'Expected start profiling result object');813  return {814    ...object,815    status: expectString(object.status, 'Expected start profiling status'),816    traceName: expectString(817      object.traceName,818      'Expected start profiling traceName'819    ),820  };821}822823function parseStopProfilingResult(value: mixed): StopProfilingResult {824  const object = expectObject(value, 'Expected stop profiling result object');825  return {826    ...object,827    status: expectString(object.status, 'Expected stop profiling status'),828    traceName: expectString(829      object.traceName,830      'Expected stop profiling traceName'831    ),832    commits: expectNumber(object.commits, 'Expected stop profiling commits'),833  };834}835836function parseTraceOverview(value: mixed): Array<TraceOverviewCommit> {837  return expectArray(value, 'Expected trace overview array').map(838    (commit, index) => {839      const object = expectObject(commit, `Expected trace overview ${index}`);840      return {841        ...object,842        commit: expectNumber(843          object.commit,844          `Expected trace overview ${index} commit`845        ),846        componentsChanged: expectNumber(847          object.componentsChanged,848          `Expected trace overview ${index} componentsChanged`849        ),850      };851    }852  );853}854855function parseCommitReport(value: mixed): CommitReport {856  const object = expectObject(value, 'Expected commit report object');857  return {858    ...object,859    components: expectArray(860      object.components,861      'Expected commit report components'862    ).map((component, index) =>863      parseNamedObject(component, `Expected commit report component ${index}`)864    ),865  };866}867868function findNode(869  tree: Array<TreeNode>,870  predicate: (node: TreeNode) => boolean,871  message: string872): TreeNode {873  const node = tree.find(predicate);874  if (node == null) {875    throw createError(876      `${message}. Saw: ${tree877        .map(item => `${item.name}:${item.type}`)878        .join(', ')}`879    );880  }881  return node;882}883884function flattenSnapshot(885  node: SnapshotNode,886  result: Array<SnapshotNode>887): Array<SnapshotNode> {888  result.push(node);889  // eslint-disable-next-line no-for-of-loops/no-for-of-loops890  for (const child of node.children || []) {891    flattenSnapshot(child, result);892  }893  return result;894}895896function findSnapshotNode(897  snapshot: SnapshotNode,898  predicate: (node: SnapshotNode) => boolean,899  message: string900): SnapshotNode {901  const node = flattenSnapshot(snapshot, []).find(predicate);902  if (node == null) {903    throw createError(message);904  }905  return node;906}907908function assertHasSchema(tool: ToolDefinition): void {909  assert.strictEqual(typeof tool.description, 'string');910  assert(tool.description.length > 0);911  assert.strictEqual(tool.inputSchema.type, 'object');912}913914function getReactToolGroup(discovery: ToolDiscovery): ToolGroup | null {915  const toolGroups = discovery.thirdPartyDeveloperTools;916  if (Array.isArray(toolGroups)) {917    return toolGroups.find(group => group.name === 'react') || null;918  }919  if (920    toolGroups != null &&921    typeof toolGroups === 'object' &&922    toolGroups.name === 'react'923  ) {924    return toolGroups;925  }926  return null;927}928929function assertSourceReference(sourceResult: SourceResult): void {930  if (sourceResult.source === null) {931    return;932  }933  const source = sourceResult.source;934  assert.strictEqual(typeof source.name, 'string');935  assert.strictEqual(typeof source.fileName, 'string');936  assert(937    /App\.js|bundle\.js|webpack/.test(source.fileName),938    `Expected source file to reference App.js, bundle.js, or webpack. Saw: ${source.fileName}`939  );940  assert.strictEqual(typeof source.line, 'number');941  assert.strictEqual(typeof source.column, 'number');942}943944async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {945  await chrome.json(['navigate_page', '--type', 'url', '--url', appUrl]);946  await waitForPageReady(chrome, 30000);947948  log('Checking third-party tool discovery...');949  const discovery = parseToolDiscovery(950    await chrome.json(['list_3p_developer_tools'])951  );952  const toolGroup = getReactToolGroup(discovery);953  if (toolGroup == null) {954    throw createError('Expected a third-party tool group');955  }956  assert.strictEqual(toolGroup.name, 'react');957  assert.deepStrictEqual(958    toolGroup.tools.map(tool => tool.name),959    TOOL_NAMES960  );961  // eslint-disable-next-line no-for-of-loops/no-for-of-loops962  for (const tool of toolGroup.tools) {963    assertHasSchema(tool);964  }965  const domTool = toolGroup.tools.find(966    tool => tool.name === 'react_get_component_by_dom_element'967  );968  if (domTool == null) {969    throw createError(970      'Expected react_get_component_by_dom_element in discovery'971    );972  }973  const inputSchemaProperties = domTool.inputSchema.properties;974  if (inputSchemaProperties == null) {975    throw createError('Expected DOM lookup tool schema properties');976  }977  const domElementSchema = inputSchemaProperties.element;978  if (domElementSchema == null) {979    throw createError('Expected DOM lookup tool element schema');980  }981  const domElementSchemaProperties = domElementSchema.properties;982  if (domElementSchemaProperties == null) {983    throw createError('Expected DOM lookup tool element schema properties');984  }985  assert.strictEqual(domElementSchema.type, 'object');986  assert.deepStrictEqual(domElementSchemaProperties, {uid: {type: 'string'}});987  assert.deepStrictEqual(domElementSchema.required, ['uid']);988989  log('Checking tree, details, search, and DOM lookup...');990  const callTool = (toolName: string, params?: {...}): Promise<mixed> =>991    chrome992      .json([993        'execute_3p_developer_tool',994        toolName,995        '--params',996        JSON.stringify(params || {}),997      ])998      .then(parseToolResponse);9991000  const tree = parseTree(await callTool('react_get_component_tree'));1001  const counter = findNode(1002    tree,1003    node => node.name === 'Counter' && node.type === 'function',1004    'Expected function component Counter'1005  );1006  const todo = findNode(1007    tree,1008    node => node.name === 'Todo' && node.type === 'function',1009    'Expected function component Todo'1010  );1011  const todoList = findNode(1012    tree,1013    node => node.name === 'TodoList' && node.type === 'function',1014    'Expected function component TodoList'1015  );1016  const todoListHost = findNode(1017    tree,1018    node => node.name === 'ul' && node.type === 'host',1019    'Expected host ul for TodoList'1020  );1021  const mainNode = findNode(1022    tree,1023    node => node.name === 'main' && node.type === 'host',1024    'Expected host main'1025  );1026  const app = findNode(1027    tree,1028    node => node.name === 'App' && node.type === 'function',1029    'Expected function component App'1030  );1031  const root = findNode(1032    tree,1033    node => node.type === 'root',1034    'Expected root node'1035  );1036  const memoBox = findNode(1037    tree,1038    node => node.name.includes('MemoBox') && node.type === 'memo',1039    'Expected memo component MemoBox'1040  );1041  const fancyInput = findNode(1042    tree,1043    node => node.name.includes('FancyInput') && node.type === 'forwardRef',1044    'Expected forwardRef component FancyInput'1045  );1046  const input = findNode(1047    tree,1048    node => node.name === 'input' && node.type === 'host',1049    'Expected host input'1050  );10511052  const counterDetails = parseComponentDetails(1053    await callTool('react_get_component_by_uid', {1054      uid: counter.uid,1055      includeHooks: true,1056    })1057  );1058  assert.strictEqual(counterDetails.name, 'Counter');1059  assert(1060    counterDetails.hooks.some(hook => hook.name === 'State'),1061    'Expected Counter details to include a State hook'1062  );10631064  assert.strictEqual(1065    parseComponentType(1066      await callTool('react_get_component_by_uid', {uid: memoBox.uid})1067    ),1068    'memo'1069  );1070  assert.strictEqual(1071    parseComponentType(1072      await callTool('react_get_component_by_uid', {uid: fancyInput.uid})1073    ),1074    'forwardRef'1075  );1076  assert.strictEqual(1077    parseComponentType(1078      await callTool('react_get_component_by_uid', {uid: input.uid})1079    ),1080    'host'1081  );10821083  const todoSearch = parseSearchResult(1084    await callTool('react_find_components', {1085      name: 'Todo',1086      pageSize: 2,1087    })1088  );1089  assert.strictEqual(todoSearch.page, 1);1090  assert.strictEqual(todoSearch.pageSize, 2);1091  assert.strictEqual(todoSearch.totalCount, 4);1092  assert.strictEqual(todoSearch.totalPages, 2);1093  assert.deepStrictEqual(1094    todoSearch.results.map(result => result.name),1095    ['TodoList', 'Todo']1096  );10971098  const snapshot = parseSnapshotResponse(await chrome.json(['take_snapshot']));1099  const buttonNode = findSnapshotNode(1100    snapshot.snapshot,1101    node => node.role === 'button' && node.name === '+1',1102    'Expected +1 button in snapshot'1103  );1104  const buttonUid = buttonNode.id;1105  if (buttonUid == null) {1106    throw createError('Expected +1 button uid');1107  }1108  const domLookup = parseDomLookupResult(1109    await callTool('react_get_component_by_dom_element', {1110      element: {uid: buttonUid},1111    })1112  );1113  assert.strictEqual(domLookup.type, 'host');1114  assert.strictEqual(domLookup.name, 'button');11151116  log('Checking source, parents, owners, and error payloads...');1117  const source = parseSourceResult(1118    await callTool('react_get_component_source', {1119      uid: counter.uid,1120    })1121  );1122  assertSourceReference(source);11231124  const ownersStack = parseOwnersStack(1125    await callTool('react_get_owner_stack_trace', {uid: todo.uid})1126  );1127  assert.strictEqual(typeof ownersStack.stack, 'string');1128  if (ownersStack.stack.length > 0) {1129    assert(1130      /Todo|TodoList|App\.js/.test(ownersStack.stack),1131      `Expected owners stack to reference Todo, TodoList, or App.js. Saw: ${ownersStack.stack}`1132    );1133  }11341135  const parentsBranch = parseComponentBranch(1136    await callTool('react_get_parent_stack', {1137      uid: todo.uid,1138    }),1139    'parents'1140  );1141  assert.deepStrictEqual(parentsBranch, [1142    {1143      uid: todoListHost.uid,1144      name: todoListHost.name,1145      type: todoListHost.type,1146    },1147    {1148      uid: todoList.uid,1149      name: todoList.name,1150      type: todoList.type,1151    },1152    {1153      uid: mainNode.uid,1154      name: mainNode.name,1155      type: mainNode.type,1156    },1157    {1158      uid: app.uid,1159      name: app.name,1160      type: app.type,1161    },1162    {1163      uid: root.uid,1164      name: root.name,1165      type: root.type,1166    },1167  ]);11681169  const ownersBranch = parseComponentBranch(1170    await callTool('react_get_owner_stack', {1171      uid: todo.uid,1172    }),1173    'owners'1174  );1175  assert(1176    ownersBranch.some(owner => owner.name === 'TodoList'),1177    'Expected Todo owners branch to include TodoList'1178  );11791180  const invalidUid = parseErrorPayload(1181    await callTool('react_get_component_by_uid', {1182      uid: 'r999999',1183    })1184  );1185  assert.deepStrictEqual(invalidUid, {1186    error: 'Component not found: "r999999"',1187  });11881189  log('Checking profiling through a real CLI click...');1190  const traceName = `e2e-${Date.now()}`;1191  assert.deepStrictEqual(1192    parseStartProfilingResult(1193      await callTool('react_start_profiling', {traceName})1194    ),1195    {1196      status: 'started',1197      traceName,1198    }1199  );1200  await chrome.json(['click', buttonUid]);1201  const stopResult = parseStopProfilingResult(1202    await callTool('react_stop_profiling')1203  );1204  assert.strictEqual(stopResult.status, 'stopped');1205  assert.strictEqual(stopResult.traceName, traceName);1206  assert(1207    stopResult.commits >= 1,1208    'Expected profiling to record at least one commit'1209  );12101211  const overview = parseTraceOverview(1212    await callTool('react_get_trace_overview', {traceName})1213  );1214  assert(overview.length >= 1, 'Expected at least one profiling commit');1215  assert(1216    overview.some(commit => commit.componentsChanged >= 1),1217    'Expected at least one changed component in trace overview'1218  );12191220  let foundCounterCommit = false;1221  // eslint-disable-next-line no-for-of-loops/no-for-of-loops1222  for (const commit of overview) {1223    const report = parseCommitReport(1224      await callTool('react_get_commit_report', {1225        traceName,1226        commitIndex: commit.commit,1227      })1228    );1229    if (report.components.some(component => component.name === 'Counter')) {1230      foundCounterCommit = true;1231      break;1232    }1233  }1234  assert(foundCounterCommit, 'Expected a profiling commit involving Counter');1235}12361237async function main(): Promise<void> {1238  ensureBuiltModules();1239  fs.mkdirSync(LOG_DIR, {recursive: true});12401241  const fixtureLog = path.join(LOG_DIR, 'fixture-server.log');1242  const chromeLog = path.join(LOG_DIR, 'chrome-devtools.log');1243  const cliLog = path.join(LOG_DIR, 'chrome-devtools-cli.log');1244  fs.writeFileSync(fixtureLog, '');1245  fs.writeFileSync(chromeLog, '');1246  fs.writeFileSync(cliLog, '');12471248  const chromeDevToolsBin = getChromeDevToolsBin();1249  const port = await getFreePort();1250  const appUrl = `http://127.0.0.1:${port}/`;12511252  let fixture: ?ChildProcess;1253  let debuggableChrome: ?ChildProcess;1254  let debuggableChromeProfile: ?string;1255  const runChrome = (args: Array<string>): Promise<CommandResult> =>1256    runCommand(1257      process.execPath,1258      [chromeDevToolsBin, ...args, '--sessionId', SESSION_ID],1259      {1260        cwd: PACKAGE_DIR,1261        env: process.env,1262        logFile: cliLog,1263        timeout: 120000,1264      }1265    );1266  const chrome: Chrome = {1267    run: runChrome,1268    async json(args: Array<string>): Promise<mixed> {1269      const result = await runChrome([...args, '--output-format', 'json']);1270      return parseJsonOutput(result.stdout);1271    },1272  };12731274  try {1275    log(`Starting fixture at ${appUrl}`);1276    fixture = spawnLogged('yarn', ['start'], {1277      cwd: FIXTURE_DIR,1278      detached: true,1279      env: {1280        ...process.env,1281        BROWSER: 'none',1282        CI: 'true',1283        E2E: 'true',1284        HOST: '127.0.0.1',1285        PORT: String(port),1286      },1287      logFile: fixtureLog,1288    });1289    await waitForHttp(appUrl, 60000);12901291    const startArgs = [1292      'start',1293      '--categoryExperimentalThirdParty=true',1294      '--usageStatistics=false',1295      '--logFile',1296      chromeLog,1297    ];1298    const chromeExecutablePath = process.env.CHROME_EXECUTABLE_PATH;1299    if (chromeExecutablePath != null) {1300      const remoteDebuggingPort = await getFreePort();1301      const launchedChrome = startDebuggableChrome(1302        chromeExecutablePath,1303        remoteDebuggingPort,1304        chromeLog1305      );1306      debuggableChrome = launchedChrome.process;1307      debuggableChromeProfile = launchedChrome.profileDir;1308      const browserUrl = `http://127.0.0.1:${remoteDebuggingPort}`;1309      await waitForHttp(`${browserUrl}/json/version`, 30000);1310      startArgs.push('--browserUrl', browserUrl);1311    } else {1312      startArgs.push('--headless=true', '--isolated=true');1313    }1314    log('Starting chrome-devtools daemon...');1315    await chrome.run(startArgs);13161317    await runE2E(chrome, appUrl);1318    log('react-devtools-cdt-mcp E2E passed.');1319  } finally {1320    try {1321      await chrome.run(['stop']);1322    } catch (error) {1323      appendLog(cliLog, `Failed to stop chrome-devtools: ${error.stack}\n`);1324    }1325    if (debuggableChrome) {1326      try {1327        debuggableChrome.kill('SIGTERM');1328        await waitForExit(debuggableChrome, 5000);1329      } catch (error) {1330        appendLog(chromeLog, `Failed to stop Chrome: ${error.stack}\n`);1331      }1332    }1333    if (debuggableChromeProfile) {1334      await removePathWithRetries(debuggableChromeProfile, chromeLog);1335    }1336    if (fixture && fixture.pid) {1337      try {1338        process.kill(-fixture.pid, 'SIGTERM');1339      } catch (error) {1340        try {1341          fixture.kill('SIGTERM');1342        } catch (innerError) {1343          appendLog(1344            fixtureLog,1345            `Failed to stop fixture server: ${innerError.stack}\n`1346          );1347        }1348      }1349    }1350  }1351}13521353module.exports = {main};

Code quality findings 97

Can lead to race conditions; consider Promises or async/await for better control
info correctness set-timeout
return new Promise(resolve => setTimeout(resolve, ms));
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (address == null || typeof address === 'string') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (address == null || typeof address === 'string') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
detached: options.detached === true,
Can lead to race conditions; consider Promises or async/await for better control
info correctness set-timeout
const timer = setTimeout(() => {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.exitCode == null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (child.exitCode != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (attempt === 4) {
Can lead to race conditions; consider Promises or async/await for better control
info correctness set-timeout
const timer = setTimeout(() => {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (code === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (process.platform === 'linux') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
packageJson == null ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof packageJson !== 'object' ||
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof packageJson !== 'object' ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
packageJson.bin == null ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof packageJson.bin !== 'object' ||
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof packageJson.bin !== 'object' ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof packageJson.bin['chrome-devtools'] !== 'string'
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof packageJson.bin['chrome-devtools'] !== 'string'
Ensure try blocks have corresponding catch or finally blocks
info correctness try-without-catch
try {
Can lead to race conditions; consider Promises or async/await for better control
info correctness set-timeout
request.setTimeout(1000, () => {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
hasApp: document.querySelector('main.app') !== null,
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
hasHook: window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null,
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
hasDiscoveryListener: window.__dtmcp != null ||
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null,
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (result.hasApp === true && result.hasHook === true) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
parsed.length === 1 &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
parsed[0].type === 'text' &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof parsed[0].text === 'string'
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof parsed[0].text === 'string'
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof response === 'string') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof response === 'string') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (response != null && typeof response === 'object') {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (response != null && typeof response === 'object') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (response != null && typeof response === 'object') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof message === 'string') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof message === 'string') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof value !== 'string') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof value !== 'string') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (typeof value !== 'number') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (typeof value !== 'number') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (value == null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.type != null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.properties != null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.required != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (rawToolGroups == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
hasApp: object.hasApp === true,
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
hasHook: object.hasHook === true,
Ensure all async functions handle errors properly
info correctness async-without-catch
async function evaluatePageReadiness(
Ensure try blocks have corresponding catch or finally blocks
info correctness try-without-catch
): ComponentBranchEntry {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.id != null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.role != null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.name != null) {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (object.children != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (object.source == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (node == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (node == null) {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
assert.strictEqual(typeof tool.description, 'string');
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
return toolGroups.find(group => group.name === 'react') || null;
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
toolGroups != null &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
typeof toolGroups === 'object' &&
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
typeof toolGroups === 'object' &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
toolGroups.name === 'react'
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (sourceResult.source === null) {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
assert.strictEqual(typeof source.name, 'string');
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
assert.strictEqual(typeof source.fileName, 'string');
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
assert.strictEqual(typeof source.line, 'number');
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
assert.strictEqual(typeof source.column, 'number');
Ensure all async functions handle errors properly
info correctness async-without-catch
async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (toolGroup == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
tool => tool.name === 'react_get_component_by_dom_element'
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (domTool == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (inputSchemaProperties == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (domElementSchema == null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (domElementSchemaProperties == null) {
Chain Promises properly to avoid callback hell
info correctness unresolved-promise
.then(parseToolResponse);
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'Counter' && node.type === 'function',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'Todo' && node.type === 'function',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'TodoList' && node.type === 'function',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'ul' && node.type === 'host',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'main' && node.type === 'host',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'App' && node.type === 'function',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.type === 'root',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name.includes('MemoBox') && node.type === 'memo',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name.includes('FancyInput') && node.type === 'forwardRef',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.name === 'input' && node.type === 'host',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
counterDetails.hooks.some(hook => hook.name === 'State'),
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
node => node.role === 'button' && node.name === '+1',
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (buttonUid == null) {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
assert.strictEqual(typeof ownersStack.stack, 'string');
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
ownersBranch.some(owner => owner.name === 'TodoList'),
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (report.components.some(component => component.name === 'Counter')) {
Ensure all async functions handle errors properly
info correctness async-without-catch
async function main(): Promise<void> {
Ensure try blocks have corresponding catch or finally blocks
info correctness try-without-catch
try {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (chromeExecutablePath != null) {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.