diff --git a/eslint.config.js b/eslint.config.js index 8653f3161..54f571a3c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,10 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import js from '@eslint/js'; import stylisticPlugin from '@stylistic/eslint-plugin'; import {defineConfig, globalIgnores} from 'eslint/config'; +import eslintPlugin from 'eslint-plugin-eslint-plugin'; import importPlugin from 'eslint-plugin-import'; +import jsdocPlugin from 'eslint-plugin-jsdoc'; +import mochaPlugin from 'eslint-plugin-mocha'; import globals from 'globals'; import tseslint from 'typescript-eslint'; @@ -33,8 +35,18 @@ export default defineConfig([ 'tests/fixtures/', 'src/third_party/lighthouse-devtools-mcp-bundle.js', ]), - importPlugin.flatConfigs.typescript, { + name: 'JavaScript files', + plugins: { + '@local': localPlugin, + '@typescript-eslint': tseslint.plugin, + '@stylistic': stylisticPlugin, + '@eslint-plugin': eslintPlugin, + mocha: mochaPlugin, + import: importPlugin, + jsdoc: jsdocPlugin, + }, + languageOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -42,26 +54,11 @@ export default defineConfig([ globals: { ...globals.node, }, - - parserOptions: { - projectService: { - allowDefaultProject: [ - 'prettier.config.js', - 'puppeteer.config.js', - 'eslint.config.js', - 'rollup.config.js', - ], - }, - }, - - parser: tseslint.parser, }, - plugins: { - js, - '@local': localPlugin, - '@typescript-eslint': tseslint.plugin, - '@stylistic': stylisticPlugin, + linterOptions: { + reportUnusedDisableDirectives: 'error', + reportUnusedInlineConfigs: 'error', }, settings: { @@ -70,74 +67,437 @@ export default defineConfig([ }, }, - extends: ['js/recommended'], - }, - tseslint.configs.recommended, - tseslint.configs.stylistic, - { - name: 'TypeScript rules', rules: { - '@local/check-license': 'error', - curly: ['error', 'all'], + // syntax preferences + '@stylistic/quotes': [ + 'error', + 'single', + { + avoidEscape: true, + allowTemplateLiterals: 'always', + }, + ], - // ESLint 10 newly recommends these rules. Disable them explicitly so this - // dependency upgrade does not require unrelated source changes. - 'no-useless-assignment': 'off', - 'preserve-caught-error': 'off', + '@stylistic/semi': 'error', + '@stylistic/no-extra-semi': 'error', + '@stylistic/comma-style': ['error', 'last'], + '@stylistic/comma-dangle': ['error', 'always-multiline'], + '@stylistic/wrap-iife': ['error', 'inside'], - 'no-undef': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ + '@stylistic/spaced-comment': [ 'error', + 'always', { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', + markers: ['*'], }, ], - '@typescript-eslint/no-explicit-any': [ + + eqeqeq: 'error', + + 'accessor-pairs': [ 'error', { - ignoreRestArgs: true, + getWithoutSet: false, + setWithoutGet: false, }, ], - // This optimizes the dependency tracking for type-only files. - '@typescript-eslint/consistent-type-imports': 'error', - // So type-only exports get elided. - '@typescript-eslint/consistent-type-exports': 'error', - // Prefer interfaces over types for shape like. - '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - '@typescript-eslint/array-type': [ + + curly: 'error', + '@stylistic/new-parens': 'error', + '@stylistic/function-call-spacing': 'error', + '@stylistic/arrow-parens': ['error', 'as-needed'], + '@stylistic/eol-last': 'error', + 'object-shorthand': ['error', 'properties'], + 'no-useless-rename': 'error', + + // anti-patterns + 'no-caller': 'error', + 'no-case-declarations': 'error', + 'no-cond-assign': 'error', + + 'no-console': [ 'error', { - default: 'array-simple', + allow: [ + 'assert', + 'context', + 'error', + 'timeStamp', + 'time', + 'timeEnd', + 'warn', + ], + }, + ], + + 'no-debugger': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + + 'no-else-return': [ + 'error', + { + allowElseIf: false, + }, + ], + + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, }, ], - '@typescript-eslint/no-floating-promises': 'error', + 'no-lonely-if': 'error', + + 'no-empty-character-class': 'error', + 'no-global-assign': 'error', + 'no-implied-eval': 'error', + 'no-labels': 'error', + 'no-multi-str': 'error', + 'no-object-constructor': 'error', + 'no-octal-escape': 'error', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-unreachable': 'error', + 'no-unsafe-negation': 'error', + + 'no-unused-vars': [ + 'error', + { + args: 'none', + vars: 'local', + }, + ], + + 'no-var': 'error', + 'no-with': 'error', + 'prefer-const': 'error', + radix: 'error', + 'valid-typeof': 'error', + 'no-return-assign': ['error', 'always'], + 'no-implicit-coercion': ['error', {allow: ['!!']}], + + 'no-array-constructor': 'error', + + // es2015 features + 'require-yield': 'error', + '@stylistic/template-curly-spacing': ['error', 'never'], - // Incompatible with ESLint 10 + // file whitespace + '@stylistic/no-multiple-empty-lines': [ + 'error', + { + max: 1, + }, + ], + '@stylistic/no-mixed-spaces-and-tabs': 'error', + '@stylistic/no-trailing-spaces': 'error', + '@stylistic/linebreak-style': ['error', 'unix'], + + /** + * Disabled, aspirational rules + */ + '@stylistic/indent': [ + 'off', + 2, + { + SwitchCase: 1, + CallExpression: { + arguments: 2, + }, + MemberExpression: 2, + }, + ], + + // brace-style is disabled, as eslint cannot enforce 1tbs as default, but allman for functions + '@stylistic/brace-style': [ + 'off', + 'allman', + { + allowSingleLine: true, + }, + ], + + // key-spacing is disabled, as some objects use value-aligned spacing, some not. + '@stylistic/key-spacing': [ + 'off', + { + beforeColon: false, + afterColon: true, + align: 'value', + }, + ], + + '@stylistic/quote-props': ['error', 'as-needed'], + + // no-implicit-globals will prevent accidental globals + 'no-implicit-globals': 'off', + 'no-unused-private-class-members': 'error', + 'no-useless-constructor': 'error', + + // Sort imports first + 'import/first': 'error', + // Closure does not properly typecheck default exports + 'no-restricted-syntax': [ + 'error', + { + selector: 'ExportDefaultDeclaration', + message: + 'Default exports are not allowed. Use named exports instead.', + }, + ], + /** + * Catch duplicate import paths. For example this would catch the following example: + * import {Foo} from './foo.js' + * import * as FooModule from './foo.js' + **/ + 'import/no-duplicates': 'error', + // Incompatible with ESLint 10 (crashes on removed + // `sourceCode.getTokenOrCommentAfter`). // 'import/order': [ // 'error', // { + // groups: [['builtin', 'external'], 'parent', 'sibling', 'index'], // 'newlines-between': 'always', - + // named: false, // alphabetize: { // order: 'asc', // caseInsensitive: true, // }, // }, // ], + 'import/enforce-node-protocol-usage': ['error', 'always'], - 'import/no-cycle': [ + /** + * Enforce some consistency and usefulness of JSDoc comments, to make sure + * we actually benefit from them. + */ + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/empty-tags': 'error', + 'jsdoc/multiline-blocks': 'error', + 'jsdoc/no-bad-blocks': 'error', + 'jsdoc/no-blank-blocks': [ 'error', { - maxDepth: Infinity, + enableFixer: true, }, ], + 'jsdoc/require-asterisk-prefix': 'error', + 'jsdoc/require-param-name': 'error', + 'jsdoc/require-hyphen-before-param-description': ['error', 'never'], + 'jsdoc/sort-tags': 'error', + }, + }, + { + name: 'TypeScript files', + files: ['**/*.ts'], - 'import/enforce-node-protocol-usage': ['error', 'always'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', - '@stylistic/function-call-spacing': 'error', - '@stylistic/semi': 'error', + parser: tseslint.parser, + parserOptions: { + allowAutomaticSingleRunInference: true, + projectService: true, + }, + }, + + rules: { + '@typescript-eslint/array-type': [ + 'error', + { + default: 'array-simple', + }, + ], + '@typescript-eslint/no-explicit-any': [ + 'error', + { + ignoreRestArgs: true, + }, + ], + + '@typescript-eslint/explicit-member-accessibility': [ + 'error', + { + accessibility: 'no-public', + }, + ], + + // run just the TypeScript unused-vars rule, else we get duplicate errors + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + ignoreUsingDeclarations: true, + }, + ], + + '@typescript-eslint/no-floating-promises': [ + 'error', + { + ignoreVoid: true, + }, + ], + + /** + * Enforce that enum members are explicitly defined: + * const enum Foo { A = 'a' } rather than const enum Foo { A } + */ + '@typescript-eslint/prefer-enum-initializers': 'error', + /** + * Ban non-null assertion operator, e.g.: + * this.foo!.toLowerCase() + */ + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + + '@typescript-eslint/naming-convention': [ + 'error', + // Forbids interfaces starting with an I prefix. + { + selector: 'interface', + format: ['PascalCase'], + + custom: { + regex: '^I[A-Z]', + match: false, + }, + }, + { + selector: [ + 'function', + 'accessor', + 'method', + 'property', + 'parameterProperty', + ], + format: ['camelCase'], + }, + { + selector: 'variable', + format: ['camelCase'], + }, + { + // We are using camelCase, PascalCase and UPPER_CASE for top-level constants, allow the for now. + selector: 'variable', + modifiers: ['const'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + }, + { + selector: 'classProperty', + modifiers: ['static', 'readonly'], + format: ['UPPER_CASE', 'camelCase'], + }, + { + selector: 'enumMember', + format: ['UPPER_CASE'], + }, + { + selector: ['typeLike'], + format: ['PascalCase'], + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { + // Public methods are currently in transition and may still have leading underscores. + selector: 'method', + modifiers: ['public'], + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { + selector: 'property', + modifiers: ['public'], + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { + // Object literals may be constructed as arguments to external libraries which follow different styles. + selector: ['objectLiteralMethod', 'objectLiteralProperty'], + modifiers: ['public'], + format: null, + }, + { + // Ignore type properties that require quotes + selector: 'typeProperty', + format: null, + modifiers: ['requiresQuotes'], + }, + ], + + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + + // Disable eslint base rule + 'no-throw-literal': 'off', + '@typescript-eslint/only-throw-error': 'error', + + // Disabled this rule while investigating why it creates + // certain TypeScript compilation errors after fixes + '@typescript-eslint/no-unnecessary-type-assertion': 'off', + + '@typescript-eslint/consistent-generic-constructors': 'off', + + // This is more performant + // And should provide better stack trace when debugging + // see https://v8.dev/blog/fast-async. + '@typescript-eslint/return-await': ['error', 'always'], + + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + // Change after we add some placeholder for old errors + minimumDescriptionLength: 0, + 'ts-check': false, + 'ts-expect-error': 'allow-with-description', + 'ts-ignore': true, + 'ts-nocheck': true, + }, + ], + + // Broke in 8.58.0, waiting for a fix + // https://github.com/typescript-eslint/typescript-eslint/issues/12204 + '@typescript-eslint/prefer-optional-chain': 'off', + + '@typescript-eslint/no-unsafe-function-type': 'error', + + '@typescript-eslint/no-empty-object-type': [ + 'error', + { + allowInterfaces: 'with-single-extends', + }, + ], + + 'no-array-constructor': 'off', + '@typescript-eslint/no-array-constructor': 'error', + + '@typescript-eslint/consistent-indexed-object-style': 'error', + + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': 'error', + + // Disallow redundant (and potentially conflicting) type information + // within JSDoc comments. + 'jsdoc/no-types': 'error', + 'jsdoc/require-returns-description': 'error', + }, + }, + { + name: 'Repository rules', + rules: { + '@local/check-license': 'error', + + 'import/no-cycle': [ + 'error', + { + maxDepth: Infinity, + }, + ], 'no-restricted-imports': [ 'error', @@ -151,6 +511,14 @@ export default defineConfig([ name: 'Source files', files: ['src/**/*.ts'], rules: { + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true, + allowConciseArrowFunctionExpressionsStartingWithVoid: true, + allowIIFEs: true, + }, + ], '@local/no-direct-third-party-imports': 'error', 'no-restricted-imports': [ 'error', @@ -171,6 +539,82 @@ export default defineConfig([ '@local/require-parsed-arguments': 'error', }, }, + { + name: 'Telemetry', + files: ['src/telemetry/**/*.ts'], + rules: { + // Telemetry payloads mirror the snake_case Clearcut schema. + '@typescript-eslint/naming-convention': 'off', + }, + }, + { + name: 'CLI and daemon', + files: ['src/bin/**/*.ts', 'src/daemon/**/*.ts'], + rules: { + // These intentionally print user-facing output to stdout. + 'no-console': 'off', + }, + }, + { + name: 'Scripts files', + files: ['scripts/**/*'], + rules: { + 'no-console': 'off', + 'no-restricted-syntax': 'off', + }, + }, + { + name: 'TypeScript test files', + files: ['*.test.ts', 'tests/**/*.ts', 'scripts/eslint_rules/tests/**/*'], + rules: { + // errors on {describe, it}.only + 'mocha/no-exclusive-tests': 'error', + + 'mocha/no-async-suite': 'error', + 'mocha/no-top-level-tests': 'error', + 'mocha/no-nested-tests': 'error', + + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + + '@typescript-eslint/only-throw-error': [ + 'error', + { + allow: [ + { + // Chai AssertionError does not extend Error + from: 'package', + package: 'chai', + name: ['AssertionError'], + }, + ], + }, + ], + }, + settings: { + mocha: { + // We use `node:test`, whose functions are imported rather than + // globals, so register them as `require`-style names. + additionalCustomNames: [ + {name: 'describe', type: 'suite', interface: 'require'}, + {name: 'suite', type: 'suite', interface: 'require'}, + {name: 'it', type: 'testCase', interface: 'require'}, + {name: 'test', type: 'testCase', interface: 'require'}, + {name: 'before', type: 'hook', interface: 'require'}, + {name: 'after', type: 'hook', interface: 'require'}, + {name: 'beforeEach', type: 'hook', interface: 'require'}, + {name: 'afterEach', type: 'hook', interface: 'require'}, + ], + }, + }, + }, + { + name: 'ESLint rules tests', + files: ['scripts/eslint_rules/tests/**/*'], + rules: { + '@eslint-plugin/no-only-tests': 'error', + }, + }, { name: 'Tests', files: ['**/*.test.ts'], @@ -181,4 +625,27 @@ export default defineConfig([ '@local/enforce-using': 'error', }, }, + { + name: 'TypeScript type-definitions', + files: ['**/*.d.ts'], + rules: { + // Not a useful rule for .d.ts files where we are + // representing an existing module. + 'no-restricted-syntax': 'off', + }, + }, + { + name: 'Config files', + files: [ + 'eslint.config.js', + 'prettier.config.js', + 'puppeteer.config.js', + 'rollup.config.js', + ], + rules: { + // The config operate on the default export + // So allow it for them + 'no-restricted-syntax': 'off', + }, + }, ]); diff --git a/package-lock.json b/package-lock.json index 22e25f0ba..e48f0e01f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ }, "devDependencies": { "@blackwell-systems/gcf": "^2.2.2", - "@eslint/js": "^10.0.1", "@google/genai": "^2.0.1", "@modelcontextprotocol/client": "2.2.0", "@modelcontextprotocol/core": "2.1.0", @@ -34,7 +33,10 @@ "core-js": "3.50.0", "eslint": "^10.7.0", "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-eslint-plugin": "^7.6.2", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^64.5.4", + "eslint-plugin-mocha": "^12.0.2", "globals": "^17.0.0", "lighthouse": "13.5.0", "prettier": "^3.6.2", @@ -108,6 +110,15 @@ "module-details-from-path": "^1.0.4" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@blackwell-systems/gcf": { "version": "2.6.1", "integrity": "sha512-Yxa26qsRbno3s9wCIF8ka8Kpdm5/Pc0kkGC2NFxhR4ghFD9sqZN6DufgsA87YeGr1zhEZn5xIpYq4whk/wcnxQ==", @@ -148,6 +159,31 @@ "tslib": "^2.4.0" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.98.0", + "integrity": "sha512-QS+b7I2aaSM+gZfDz1b9E4B367JJ2qTjFXRKF289iKkh9sYdZ21Q5MiF/Duzouy3J0C4VhM3I/8f2F4+zT8gYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9", + "@typescript-eslint/types": "^8.70.0", + "comment-parser": "1.4.9", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~9.2.2" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", @@ -240,26 +276,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", @@ -1282,6 +1298,18 @@ "node": ">=18" } }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", @@ -1983,7 +2011,6 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", @@ -2001,7 +2028,6 @@ }, "node_modules/ajv-formats/node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", @@ -2018,7 +2044,6 @@ }, "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" @@ -2059,6 +2084,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/are-docs-informative": { + "version": "0.1.1", + "integrity": "sha512-sqRsNQBwbKLRX0jV5Cu5uzmtflf892n4Vukz7T659ebL4pz3mpOqCMU7lxMoBTFwnp10E3YB5ZcyHM41W5bcDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", @@ -2229,7 +2263,6 @@ }, "node_modules/axe-core": { "version": "4.13.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", "dev": true, "license": "MPL-2.0", @@ -2430,6 +2463,15 @@ "dev": true, "license": "MIT" }, + "node_modules/comment-parser": { + "version": "1.4.9", + "integrity": "sha512-+2AvZKjJaNq9GQljm3tr0utwrig5BL+jgJGvz5rDpGKNIp+ojcRXWUwBbh/sGIi1QCprR6PsKFakub+w1v+4hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/commenting": { "version": "1.1.0", "integrity": "sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==", @@ -2634,7 +2676,6 @@ }, "node_modules/devtools-protocol": { "version": "0.0.1687809", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", "dev": true, "license": "BSD-3-Clause" @@ -3069,6 +3110,22 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-eslint-plugin": { + "version": "7.6.2", + "integrity": "sha512-MWE0tzCq0VatvFzELsTfmnMZHFZEQc2lK7HjeLLFiQDUPD8M1pCDlpOuQxGBzjbFvyvz4bfYjI03HQlbS5qdQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "estraverse": "^5.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, "node_modules/eslint-plugin-import": { "version": "2.32.0", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", @@ -3143,6 +3200,169 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "64.5.4", + "integrity": "sha512-xfRhXPSSWT612muJ6XTvxuVJ8zYHv99qNgBqPVqF28aSqoy4s4XbSkrWXnYA2hl5ejC02yEpk6fqkj3BmJ6Xbg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.98.0", + "@es-joy/resolve.exports": "1.2.0", + "@typescript-eslint/utils": "^8.70.0", + "are-docs-informative": "^0.1.1", + "comment-parser": "1.4.9", + "debug": "^4.4.3", + "escape-string-regexp": "^5.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.1", + "parse-imports-exports": "^0.2.4", + "semver": "^7.8.5", + "spdx-expression-parse": "^5.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "5.0.0", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "5.0.0", + "integrity": "sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/eslint-plugin-mocha": { + "version": "12.0.2", + "integrity": "sha512-RzjnzVBoidGFjVcvzQShLbfWug5pL/6d6LZFbBZk1b5duZ0ooq6dasmpu6qLyNowUZBqaWvQbnR5U1hsKX/+5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.10.1", + "@eslint/core": "1.2.1", + "@types/estree": "1.0.9", + "globals": "17.8.0", + "json-schema-to-ts": "3.1.1", + "type-fest": "5.8.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "eslint": ">=10.2.0" + } + }, + "node_modules/eslint-plugin-mocha/node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/eslint-plugin-mocha/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-mocha/node_modules/globals": { + "version": "17.8.0", + "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-mocha/node_modules/type-fest": { + "version": "5.8.0", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", @@ -3338,7 +3558,6 @@ }, "node_modules/fast-uri": { "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ @@ -3819,6 +4038,22 @@ "node": ">= 0.4" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/http-link-header": { "version": "1.1.3", "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", @@ -4382,6 +4617,19 @@ "node": ">=12" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "9.2.2", + "integrity": "sha512-eFY7Rlscvv/BpY6UsVsjKwMhu1tJO/F4U/vC9g3yeRy/JqszuO1UP6Vdq+85G+3FP6fEw/sWR8a5ftPFY86csw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9", + "@types/node": "^26.4.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", @@ -4397,6 +4645,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", @@ -4473,7 +4734,6 @@ }, "node_modules/lighthouse": { "version": "13.5.0", - "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-13.5.0.tgz", "integrity": "sha512-w/yYIwg7mP9pCwkcbAXRztnvKHua2dJnZaNptPLa1cEw+akr6lbsIDiUOLsQwvaiAUr0xvxdmwvAzl3pj3Np0A==", "dev": true, "license": "Apache-2.0", @@ -4534,7 +4794,6 @@ }, "node_modules/lighthouse/node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", @@ -4580,7 +4839,6 @@ }, "node_modules/lighthouse/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" @@ -4889,6 +5147,12 @@ "node": ">= 12" } }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", @@ -5086,6 +5350,21 @@ "url": "https://github.com/sponsors/dword-design" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", @@ -5313,7 +5592,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", @@ -5334,6 +5612,18 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "1.22.10", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", @@ -5969,6 +6259,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/third-party-web": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.30.0.tgz", @@ -5994,14 +6296,12 @@ }, "node_modules/tldts-core": { "version": "7.4.13", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.13.tgz", "integrity": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", "dev": true, "license": "MIT" }, "node_modules/tldts-icann": { "version": "7.4.13", - "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.4.13.tgz", "integrity": "sha512-UTJXb538oP4wCyAcAE4RawUjw87qZFJ8BEob++0goalyg40zX5iP7L0mbXx3EnG1jrAsz+WDUAK613MtF0nUPA==", "dev": true, "license": "MIT", @@ -6009,6 +6309,28 @@ "tldts-core": "^7.4.13" } }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", @@ -6259,7 +6581,6 @@ }, "node_modules/web-features": { "version": "3.39.0", - "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.39.0.tgz", "integrity": "sha512-4DgSV2sNjlLMZg1XqaKceUhbKfdUPOXQ63v9XdKOv0EOB4zBIfNh16tvrxlTFAL21HIOxtk2/e+49uBK9WF+xg==", "dev": true, "license": "Apache-2.0" diff --git a/package.json b/package.json index d80b6caaa..76f48da30 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp", "devDependencies": { "@blackwell-systems/gcf": "^2.2.2", - "@eslint/js": "^10.0.1", "@google/genai": "^2.0.1", "@modelcontextprotocol/client": "2.2.0", "@modelcontextprotocol/core": "2.1.0", @@ -75,7 +74,10 @@ "core-js": "3.50.0", "eslint": "^10.7.0", "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-eslint-plugin": "^7.6.2", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^64.5.4", + "eslint-plugin-mocha": "^12.0.2", "globals": "^17.0.0", "lighthouse": "13.5.0", "prettier": "^3.6.2", diff --git a/rollup.config.js b/rollup.config.js index b3a8e8243..9ecc70e8c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -16,7 +16,7 @@ */ /** - * @fileoverview taken from {@link https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/rollup.config.mjs | chromium-bidi} + * @file taken from {@link https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/rollup.config.mjs | chromium-bidi} * and modified to specific requirement. */ @@ -195,7 +195,7 @@ const bundleDependency = ( const stringifiedDependencies = Array.from( seenDependencies.values(), ).map(dependency => { - let arr = []; + const arr = []; arr.push(`Name: ${dependency.name ?? 'N/A'}`); let url = dependency.homepage ?? dependency.repository; if (url !== null && typeof url !== 'string') { diff --git a/scripts/eslint_rules/check-license-rule.js b/scripts/eslint_rules/check-license-rule.js index a9e048612..7616d63d0 100644 --- a/scripts/eslint_rules/check-license-rule.js +++ b/scripts/eslint_rules/check-license-rule.js @@ -73,7 +73,7 @@ export default { nextToken.loc.start.line === header.loc.end.line + 1 ) { context.report({ - node: node, + node, loc: header.loc, messageId: 'emptyLine', fix(fixer) { @@ -87,7 +87,7 @@ export default { // Add header license if (!header || !header.value.includes('@license')) { context.report({ - node: node, + node, messageId: 'licenseRule', fix(fixer) { return fixer.insertTextAfterRange(insertAfter, licenseHeader); diff --git a/scripts/eslint_rules/require-parsed-arguments-rule.js b/scripts/eslint_rules/require-parsed-arguments-rule.js index fd82dd8f7..e32f45183 100644 --- a/scripts/eslint_rules/require-parsed-arguments-rule.js +++ b/scripts/eslint_rules/require-parsed-arguments-rule.js @@ -87,9 +87,8 @@ export default { const output = `${left}: ParsedArguments = ${right}`; if (hasParens) { return fixer.replaceText(firstParam, output); - } else { - return fixer.replaceText(firstParam, `(${output})`); } + return fixer.replaceText(firstParam, `(${output})`); } else { const text = sourceCode.getText(firstParam); paramName = text.split(':')[0].trim(); @@ -101,12 +100,11 @@ export default { firstParam, `${paramName}: ParsedArguments`, ); - } else { - return fixer.replaceText( - firstParam, - `(${paramName}: ParsedArguments)`, - ); } + return fixer.replaceText( + firstParam, + `(${paramName}: ParsedArguments)`, + ); }, }); } diff --git a/scripts/eval_gemini.ts b/scripts/eval_gemini.ts index 7d748d5d3..3ad738b60 100644 --- a/scripts/eval_gemini.ts +++ b/scripts/eval_gemini.ts @@ -15,12 +15,12 @@ import {Client} from '@modelcontextprotocol/client'; import {StdioClientTransport} from '@modelcontextprotocol/client/stdio'; import {TestServer} from '../build/tests/server.js'; +import type {CapturedFunctionCall, TestScenario} from './eval_result.js'; +import {Result} from './eval_result.js'; + const ROOT_DIR = path.resolve(import.meta.dirname, '..'); const SCENARIOS_DIR = path.join(import.meta.dirname, 'eval_scenarios'); const SKILL_PATH = path.join(ROOT_DIR, 'skills', 'chrome-devtools', 'SKILL.md'); - -import type {CapturedFunctionCall, TestScenario} from './eval_result.js'; -import {Result} from './eval_result.js'; export type {CapturedFunctionCall, TestScenario}; export {Result}; diff --git a/scripts/eval_result.ts b/scripts/eval_result.ts index b4b3734bb..dbd4a38aa 100644 --- a/scripts/eval_result.ts +++ b/scripts/eval_result.ts @@ -13,10 +13,10 @@ export interface CapturedFunctionCall { export class Result { private nextCallIndex = 0; - public readonly calls: CapturedFunctionCall[]; - public readonly serverArgs: string[]; + readonly calls: CapturedFunctionCall[]; + readonly serverArgs: string[]; /** The model's final response text. */ - public readonly text: string; + readonly text: string; constructor(calls: CapturedFunctionCall[], serverArgs: string[], text = '') { this.calls = calls; diff --git a/scripts/eval_scenarios/page_focus_keyboard_test.ts b/scripts/eval_scenarios/page_focus_keyboard_test.ts index 9812b8686..8ac097650 100644 --- a/scripts/eval_scenarios/page_focus_keyboard_test.ts +++ b/scripts/eval_scenarios/page_focus_keyboard_test.ts @@ -46,7 +46,9 @@ Now use the press_key tool to type "a" on Page 1 without selecting it first. You pressKeys.length >= 2, 'Should retry press_key after error recovery', ); - const lastPressKeyIndex = result.calls.lastIndexOf(pressKeys.at(-1)!); + const lastPressKey = pressKeys.at(-1); + assert.ok(lastPressKey); + const lastPressKeyIndex = result.calls.lastIndexOf(lastPressKey); assert.ok( firstSelectPageIndex < lastPressKeyIndex, 'select_page should precede the successful press_key', diff --git a/scripts/test.js b/scripts/test.js index b31901629..fe4b8f887 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -7,7 +7,7 @@ // Note: can be converted to ts file once node 20 support is dropped. // Node 20 does not support --experimental-strip-types flag. -import {spawn, execSync} from 'node:child_process'; +import {spawn} from 'node:child_process'; import {readFile} from 'node:fs/promises'; import path from 'node:path'; import process from 'node:process'; @@ -37,22 +37,20 @@ if (userArgs.length > 0) { } files.push(testPath); } -} else { - if (flags.includes('--test-only')) { - const {glob} = await import('node:fs/promises'); - for await (const tsFile of glob('tests/**/*.test.ts')) { - const content = await readFile(tsFile, 'utf8'); - if (content.includes('.only(')) { - files.push(path.join('build', tsFile.replace(/\.ts$/, '.js'))); - } - } - if (files.length === 0) { - console.warn('no files contain .only'); - process.exit(0); +} else if (flags.includes('--test-only')) { + const {glob} = await import('node:fs/promises'); + for await (const tsFile of glob('tests/**/*.test.ts')) { + const content = await readFile(tsFile, 'utf8'); + if (content.includes('.only(')) { + files.push(path.join('build', tsFile.replace(/\.ts$/, '.js'))); } - } else if (files.length === 0) { - files.push('build/tests/**/*.test.js'); } + if (files.length === 0) { + console.warn('no files contain .only'); + process.exit(0); + } +} else if (files.length === 0) { + files.push('build/tests/**/*.test.js'); } const nodeArgs = [ @@ -68,24 +66,11 @@ const nodeArgs = [ ...files, ]; -function _installChrome(version) { - try { - return execSync( - `npx puppeteer browsers install chrome@${version} --format "{{path}}"`, - ) - .toString() - .trim(); - } catch (e) { - console.error(`Failed to install Chrome ${version}:`, e); - process.exit(1); - } -} - async function runTests(attempt) { if (attempt > 1) { console.log(`\nRun attempt ${attempt}...\n`); } - return new Promise(resolve => { + return await new Promise(resolve => { const child = spawn('node', nodeArgs, { stdio: 'inherit', env: { diff --git a/scripts/update_metrics.ts b/scripts/update_metrics.ts index 5423b2920..a027f71a7 100644 --- a/scripts/update_metrics.ts +++ b/scripts/update_metrics.ts @@ -22,7 +22,7 @@ import { } from '../build/src/telemetry/metricsRegistry.js'; import {createTools} from '../build/src/tools/tools.js'; -export function HaveUniqueNames(tools: Array<{name: string}>): boolean { +export function haveUniqueNames(tools: Array<{name: string}>): boolean { const toolNames = tools.map(tool => tool.name); const toolNamesSet = new Set(toolNames); return toolNamesSet.size === toolNames.length; @@ -39,7 +39,7 @@ function writeToolCallMetricsConfig() { // Avoid 'as ParsedArguments' by using parseArguments const allTools = createTools(new ConfigParser('0.0.0', ['', '']).parse()); - if (!HaveUniqueNames(allTools)) { + if (!haveUniqueNames(allTools)) { throw new Error('Error: Duplicate tool names found.'); } diff --git a/src/BrowserManager.ts b/src/BrowserManager.ts index e3f3b0444..409f1da62 100644 --- a/src/BrowserManager.ts +++ b/src/BrowserManager.ts @@ -141,7 +141,7 @@ export class BrowserManager { // a call silently adopt the freshly-rotated token as its own baseline // once the lock frees up, defeating the abandonment check entirely. const attempt = this.#browserAttempt; - using _guard = await this.#mutex.acquire(); + using guard = await this.#mutex.acquire(); if (this.#closingCount > 0) { throw new Error('Browser was closed while initializing.'); } @@ -518,7 +518,7 @@ export class BrowserManager { async close(): Promise { this.#initPromise = undefined; this.#closingCount++; - using _guard = await this.#mutex.acquire(); + using guard = await this.#mutex.acquire(); try { await this.#closeBrowser(); } finally { diff --git a/src/McpContext.ts b/src/McpContext.ts index 1c5319b9d..52e21f157 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -142,7 +142,7 @@ export class McpContext implements Context { ); } - async #init() { + async #init(): Promise { await this.createPagesSnapshot(); const workers = this.createWorkersSnapshot(); @@ -151,7 +151,7 @@ export class McpContext implements Context { this.browser.on('targetdestroyed', this.#onTargetDestroyed); } - dispose() { + dispose(): void { this.browser.off('targetcreated', this.#onTargetCreated); this.browser.off('targetdestroyed', this.#onTargetDestroyed); @@ -169,7 +169,7 @@ export class McpContext implements Context { this.#isolatedContexts.clear(); } - #onTargetCreated = (target: Target) => { + #onTargetCreated = (target: Target): void => { try { if (!this.#isPageTarget(target)) { return; @@ -180,7 +180,7 @@ export class McpContext implements Context { } }; - #onTargetDestroyed = (target: Target) => { + #onTargetDestroyed = (target: Target): void => { try { const mcpPage = this.#mcpPages.get(target); if (mcpPage) { @@ -199,7 +199,7 @@ export class McpContext implements Context { opts: McpContextOptions, /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */ locatorClass: typeof Locator = Locator, - ) { + ): Promise { const context = new McpContext(browser, logger, opts, locatorClass); await context.#init(); return context; @@ -452,7 +452,7 @@ export class McpContext implements Context { if (!targetPage) { return undefined; } - let timeoutId: ReturnType; + let timeoutId: ReturnType | undefined; const timeoutPromise = new Promise(resolve => { timeoutId = setTimeout(() => resolve(undefined), 500); }); @@ -462,7 +462,7 @@ export class McpContext implements Context { } catch { return undefined; } finally { - clearTimeout(timeoutId!); + clearTimeout(timeoutId); } } @@ -936,7 +936,7 @@ export class McpContext implements Context { if (!response.ok) { throw new Error(`Failed to load resource: ${url}`); } - return response.text(); + return await response.text(); } case 'file:': { diff --git a/src/McpPage.ts b/src/McpPage.ts index 5b0ae627d..8c670856d 100644 --- a/src/McpPage.ts +++ b/src/McpPage.ts @@ -16,8 +16,8 @@ import { type UncaughtError, } from './collectors/PageCollector.js'; import {TextSnapshot} from './TextSnapshot.js'; -import type {Locator} from './third_party/index.js'; import { + type Locator, PredefinedNetworkConditions, type Dialog, type ElementHandle, @@ -51,10 +51,9 @@ import { type DialogAction, } from './utils/WaitForHelper.js'; -const DEFAULT_TIMEOUT = 5_000; -const NAVIGATION_TIMEOUT = 10_000; - -export function replaceHtmlElementsWithUids(schema: JSONSchema7Definition) { +export function replaceHtmlElementsWithUids( + schema: JSONSchema7Definition, +): void { if (typeof schema === 'boolean') { return; } @@ -113,6 +112,9 @@ function isDebuggingWebMcpTool(tool: WebMCPTool): boolean { ); } +const DEFAULT_TIMEOUT = 5_000; +const NAVIGATION_TIMEOUT = 10_000; + function isBackendNodeId( id: unknown, ): id is DevTools.Protocol.DOM.BackendNodeId { @@ -255,7 +257,7 @@ export class McpPage implements ContextPage { throw new Error(`McpPage (id=${this.id}) has already been disposed.`); } if (this.#initPromise) { - return this.#initPromise; + return await this.#initPromise; } this.#initPromise = this.#doInit(); try { @@ -687,7 +689,7 @@ export class McpPage implements ContextPage { const stashedElements: Element[] = []; - const stashDOMElement = (el: Element) => { + const stashDOMElement = (el: Element): {stashedId: string} => { stashedElements.push(el); return { stashedId: `stashed-${stashedElements.length - 1}`, @@ -845,7 +847,7 @@ export class McpPage implements ContextPage { node.stashedId.startsWith('stashed-') && Object.keys(node).length === 1 ) { - const index = parseInt(node.stashedId.split('-')[1]); + const index = parseInt(node.stashedId.split('-')[1], 10); return {uid: cdpElementIds[index]}; } const resultObj: Record = {}; @@ -876,7 +878,7 @@ export class McpPage implements ContextPage { if (!node) { throw new Error(`Element uid "${uid}" not found on page ${this.id}.`); } - return this.#resolveElementHandle(node, uid); + return await this.#resolveElementHandle(node, uid); } async #resolveElementHandle( @@ -897,7 +899,7 @@ export class McpPage implements ContextPage { } } - getAXNodeByUid(uid: string) { + getAXNodeByUid(uid: string): TextSnapshotNode | undefined { return this.textSnapshot?.idToNode.get(uid); } @@ -1044,7 +1046,7 @@ export class McpPage implements ContextPage { return {}; } - async restoreEmulation() { + async restoreEmulation(): Promise { const currentSetting = this.emulationSettings; await this.emulate(currentSetting); } @@ -1164,7 +1166,7 @@ export class McpPage implements ContextPage { await page.setViewport(newSettings.viewport ?? null); } - updateTimeouts() { + updateTimeouts(): void { if (!this.#pptrPage) { return; } @@ -1205,7 +1207,7 @@ export class McpPage implements ContextPage { /** * We need to ignore favicon request as they make our test flaky */ - async setUpNetworkCollectorForTesting() { + async setUpNetworkCollectorForTesting(): Promise { this.networkCollector.dispose(); this.networkCollector = new NetworkCollector( this.pptrPage, diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 4b20b89fa..ddba368f9 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -378,7 +378,7 @@ export class McpResponse implements Response { setHeapSnapshotAggregates( aggregateData: HeapSnapshotAggregateData, options?: PaginationOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -393,7 +393,7 @@ export class McpResponse implements Response { nativeContextSizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes, retainedByContextSummary: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary, options?: HeapSnapshotFormatOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -408,7 +408,7 @@ export class McpResponse implements Response { setHeapSnapshotNodes( nodes: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange, options?: PaginationOptions & HeapSnapshotFormatOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -421,7 +421,7 @@ export class McpResponse implements Response { setHeapSnapshotDuplicateStrings( duplicateStrings: DuplicateStringGroup[], options?: PaginationOptions & HeapSnapshotFormatOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -434,7 +434,7 @@ export class McpResponse implements Response { setHeapSnapshotRetainingPaths( retainingPaths: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths, options?: HeapSnapshotFormatOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -446,7 +446,7 @@ export class McpResponse implements Response { setHeapSnapshotDominators( dominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain, options?: HeapSnapshotFormatOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -455,7 +455,7 @@ export class McpResponse implements Response { }; } - setHeapSnapshotClassDiffs(classDiffs: HeapSnapshotClassDiff[]) { + setHeapSnapshotClassDiffs(classDiffs: HeapSnapshotClassDiff[]): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -465,7 +465,7 @@ export class McpResponse implements Response { setHeapSnapshotDetailedClassDiff( detailedClassDiff: HeapSnapshotDetailedClassDiff, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -476,7 +476,7 @@ export class McpResponse implements Response { setHeapSnapshotObjectDetails( objectInfo: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo, options?: HeapSnapshotFormatOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -488,7 +488,7 @@ export class McpResponse implements Response { setHeapSnapshotContextAnalysis( contextAnalysis: DevTools.HeapSnapshotModel.HeapSnapshotModel.ContextAnalysisResult, options?: ContextAnalysisOptions, - ) { + ): void { this.#heapSnapshotOptions = { ...this.#heapSnapshotOptions, include: true, @@ -542,9 +542,8 @@ export class McpResponse implements Response { '.txt', ); return result.filename; - } else { - return formatter; } + return formatter; } async #handleAttachedNetworkRequest( @@ -591,7 +590,8 @@ export class McpResponse implements Response { fetchDetailedData: true, devTools: this.#page.devtoolsUniverse, }); - } else if (message instanceof DevTools.AggregatedIssue) { + } + if (message instanceof DevTools.AggregatedIssue) { const formatter = new IssueFormatter(message, { id: consoleMessageStableId, requestIdResolver: this.#page.resolveCdpRequestId.bind(this.#page), @@ -605,9 +605,8 @@ export class McpResponse implements Response { ); } return formatter; - } else { - return undefined; } + return undefined; } async #handleThirdPartyDevelopeTools(): Promise { @@ -805,7 +804,7 @@ export class McpResponse implements Response { extensions = await context.listExtensions(); } - return this.format( + return await this.format( context, { detailedConsoleMessage, @@ -970,9 +969,10 @@ export class McpResponse implements Response { } } - const networkConditions = this.#page?.networkConditions; - if (networkConditions) { - const timeout = this.#page!.pptrPage.getDefaultNavigationTimeout(); + const page = this.#page; + const networkConditions = page?.networkConditions; + if (page && networkConditions) { + const timeout = page.pptrPage.getDefaultNavigationTimeout(); response.push(`Emulating network conditions: ${networkConditions}`); response.push(`Default navigation timeout set to ${timeout} ms`); structuredContent.networkConditions = networkConditions; @@ -1575,7 +1575,7 @@ Call ${handleDialog(this.#args).name} to handle it before continuing.`); } if (this.#cssStylesData) { - const resolveUid = (backendNodeId: number) => + const resolveUid = (backendNodeId: number): string | undefined => this.#page?.textSnapshot?.resolveCdpElementId(backendNodeId); const containerDetails = await resolveContainerQueries( @@ -1646,7 +1646,22 @@ Call ${handleDialog(this.#args).name} to handle it before continuing.`); }; } - #dataWithPagination(data: T[], pagination?: PaginationOptions) { + #dataWithPagination( + data: T[], + pagination?: PaginationOptions, + ): { + info: string[]; + items: readonly T[]; + pagination: { + currentPage: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + startIndex: number; + endIndex: number; + invalidPage: boolean; + }; + } { const response = []; const paginationResult = paginate(data, pagination); if (paginationResult.invalidPage) { @@ -1682,7 +1697,7 @@ Call ${handleDialog(this.#args).name} to handle it before continuing.`); }; } - resetResponseLineForTesting() { + resetResponseLineForTesting(): void { this.#textResponseLines = []; } } @@ -1697,7 +1712,13 @@ function createStructuredPage( mcpPage: McpPage, context: McpContext, rawTitle: string, -) { +): { + id: number | undefined; + url: string; + title: string; + selected: boolean; + isolatedContext?: string; +} { const isolatedContextName = mcpPage.isolatedContextName; const title = truncateTitle(rawTitle); const entry: { diff --git a/src/TextSnapshot.ts b/src/TextSnapshot.ts index 4de543f59..6be61c614 100644 --- a/src/TextSnapshot.ts +++ b/src/TextSnapshot.ts @@ -17,7 +17,7 @@ import {logger} from './utils/logger.js'; export class TextSnapshot { static nextSnapshotId = 1; - static resetCounter() { + static resetCounter(): void { TextSnapshot.nextSnapshotId = 1; } @@ -155,14 +155,15 @@ export class TextSnapshot { } // TODO: index by backendNodeId instead. const queue = [this.root]; - while (queue.length) { - const current = queue.pop()!; + let current = queue.pop(); + while (current) { if (current.backendNodeId === cdpBackendNodeId) { return current.id; } for (const child of current.children) { queue.push(child); } + current = queue.pop(); } return; } @@ -268,7 +269,7 @@ export class TextSnapshot { pierce: true, }, ); - const collect = (node: Protocol.DOM.Node) => { + const collect = (node: Protocol.DOM.Node): void => { if (node.backendNodeId && node.backendNodeId !== backendNodeId) { descendantIds.add(node.backendNodeId); } diff --git a/src/ToolHandler.ts b/src/ToolHandler.ts index 49ad2b9cb..9b37480ee 100644 --- a/src/ToolHandler.ts +++ b/src/ToolHandler.ts @@ -10,7 +10,7 @@ import type {McpPage} from './McpPage.js'; import {McpResponse} from './McpResponse.js'; import {SlimMcpResponse} from './SlimMcpResponse.js'; import {ClearcutLogger} from './telemetry/ClearcutLogger.js'; -import type {Browser, CallToolResult} from './third_party/index.js'; +import type {Browser, CallToolResult, Mutex} from './third_party/index.js'; import {zod} from './third_party/index.js'; import {labels} from './tools/categories.js'; import {categoryToFlagName} from './config/category-options.js'; @@ -22,7 +22,6 @@ import type { } from './tools/ToolDefinition.js'; import {isAvailableInMode, isSlimTool} from './tools/ToolDefinition.js'; import {logger} from './utils/logger.js'; -import type {Mutex} from './third_party/index.js'; import {fileURLToPath, pathToFileURL} from 'node:url'; import {isLocalhost} from './utils/url.js'; @@ -107,7 +106,8 @@ async function validateAndResolvePathOrUrl( const url = new URL(filePathOrUrl); if (url.protocol === 'file:') { return pathToFileURL(await context.validatePath(fileURLToPath(url))).href; - } else if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) { + } + if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) { return filePathOrUrl; } } catch { @@ -234,7 +234,7 @@ export class ToolHandler { } handle = async (params: Record): Promise => { - using _guard = await this.toolMutex.acquire(); + using guard = await this.toolMutex.acquire(); if (this.disabledReason) { return { diff --git a/src/bin/chrome-devtools-mcp.ts b/src/bin/chrome-devtools-mcp.ts index 4f6a4f284..165c82d67 100644 --- a/src/bin/chrome-devtools-mcp.ts +++ b/src/bin/chrome-devtools-mcp.ts @@ -6,10 +6,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -process.title = 'chrome-devtools-mcp'; - import {version} from 'node:process'; +process.title = 'chrome-devtools-mcp'; + const [major, minor] = version.substring(1).split('.').map(Number); if (major === 20 && minor < 19) { diff --git a/src/bin/chrome-devtools.ts b/src/bin/chrome-devtools.ts index cdcf728ab..15ef405fa 100644 --- a/src/bin/chrome-devtools.ts +++ b/src/bin/chrome-devtools.ts @@ -6,8 +6,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -process.title = 'chrome-devtools'; - import process from 'node:process'; import type {Options, PositionalOptions} from 'yargs'; @@ -36,13 +34,19 @@ import {mcpOptions, getCliOptions} from '../config/mcp-options.js'; import {ConfigParser} from '../config/ConfigParser.js'; +process.title = 'chrome-devtools'; + await checkForUpdates( 'Run `npm install -g chrome-devtools-mcp@latest` and `chrome-devtools start` to update and restart the daemon.', ); const DEFAULT_CLI_ARGS = ['--viaCli']; -async function start(args: string[], sessionId: string, stopExisting = false) { +async function start( + args: string[], + sessionId: string, + stopExisting = false, +): Promise { const combinedArgs = [...DEFAULT_CLI_ARGS, ...args]; // Validates the arguments and the config file before starting the daemon. const parsedArgs = new ConfigParser(VERSION, [ diff --git a/src/collectors/PageCollector.ts b/src/collectors/PageCollector.ts index a3c80c0ed..c689ee58d 100644 --- a/src/collectors/PageCollector.ts +++ b/src/collectors/PageCollector.ts @@ -10,15 +10,13 @@ import type { ConsoleMessage, Protocol, Issue, + Frame, + Handler, + HTTPRequest, + Page, + PageEvents as PuppeteerPageEvents, } from '../third_party/index.js'; import {DevTools, FrameEvent} from '../third_party/index.js'; -import { - type Frame, - type Handler, - type HTTPRequest, - type Page, - type PageEvents as PuppeteerPageEvents, -} from '../third_party/index.js'; import { createIdGenerator, stableIdSymbol, @@ -119,7 +117,7 @@ export class PageCollector { this.#listeners = listenerMap; } - dispose() { + dispose(): void { this.#detachFrameNavigatedWithinDocumentListener(); if (this.#listeners) { for (const [name, listener] of Object.entries(this.#listeners)) { @@ -131,11 +129,11 @@ export class PageCollector { // Puppeteer emits this right before the page-level navigation event for // same-document (SPA) navigations, which lets `framenavigated` skip the // history rotation for those navigations. - #onFrameNavigatedWithinDocument = () => { + #onFrameNavigatedWithinDocument = (): void => { this.#pendingSameDocumentNavigation = true; }; - #attachFrameNavigatedWithinDocumentListener() { + #attachFrameNavigatedWithinDocumentListener(): void { this.#detachFrameNavigatedWithinDocumentListener(); this.#frameNavigatedWithinDocumentTarget = this.pptrPage.mainFrame(); this.#frameNavigatedWithinDocumentTarget.on( @@ -144,7 +142,7 @@ export class PageCollector { ); } - #detachFrameNavigatedWithinDocumentListener() { + #detachFrameNavigatedWithinDocumentListener(): void { if (this.#frameNavigatedWithinDocumentTarget) { this.#frameNavigatedWithinDocumentTarget.off( FrameEvent.FrameNavigatedWithinDocument, @@ -154,7 +152,7 @@ export class PageCollector { } } - protected splitAfterNavigation() { + protected splitAfterNavigation(): void { // Add the latest navigation first this.storage.unshift([]); this.storage.splice(this.maxNavigationSaved); @@ -247,7 +245,7 @@ class PageEventSubscriber { this.#targetId = this.#session.target()._targetId; } - #resetIssueAggregator() { + #resetIssueAggregator(): void { this.#issueManager = new FakeIssuesManager(); if (this.#issueAggregator) { this.#issueAggregator.removeEventListener( @@ -262,7 +260,7 @@ class PageEventSubscriber { ); } - subscribe() { + subscribe(): void { this.#resetIssueAggregator(); this.#page.on('framenavigated', this.#onFrameNavigated); this.#page.on('issue', this.#onIssueAdded); @@ -270,7 +268,7 @@ class PageEventSubscriber { this.#attachFrameNavigatedWithinDocumentListener(); } - unsubscribe() { + unsubscribe(): void { this.#seenKeys.clear(); this.#seenIssues.clear(); this.#detachFrameNavigatedWithinDocumentListener(); @@ -287,7 +285,7 @@ class PageEventSubscriber { #onAggregatedIssue = ( event: DevTools.Common.EventTarget.EventTargetEvent, - ) => { + ): void => { if (this.#seenIssues.has(event.data)) { return; } @@ -295,7 +293,7 @@ class PageEventSubscriber { this.#page.emit('devtoolsAggregatedIssue', event.data); }; - #onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => { + #onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent): void => { this.#page.emit( 'uncaughtError', new UncaughtError(event.exceptionDetails, this.#targetId), @@ -303,7 +301,7 @@ class PageEventSubscriber { }; // On navigation, we reset issue aggregation. - #onFrameNavigated = (frame: Frame) => { + #onFrameNavigated = (frame: Frame): void => { // Only split the storage on main frame navigation if (frame !== frame.page().mainFrame()) { return; @@ -324,11 +322,11 @@ class PageEventSubscriber { // Puppeteer emits this right before the page-level navigation event for // same-document (SPA) navigations, which lets `framenavigated` skip the // issue aggregation reset for those navigations. - #onFrameNavigatedWithinDocument = () => { + #onFrameNavigatedWithinDocument = (): void => { this.#pendingSameDocumentNavigation = true; }; - #attachFrameNavigatedWithinDocumentListener() { + #attachFrameNavigatedWithinDocumentListener(): void { this.#detachFrameNavigatedWithinDocumentListener(); this.#frameNavigatedWithinDocumentTarget = this.#page.mainFrame(); this.#frameNavigatedWithinDocumentTarget.on( @@ -337,7 +335,7 @@ class PageEventSubscriber { ); } - #detachFrameNavigatedWithinDocumentListener() { + #detachFrameNavigatedWithinDocumentListener(): void { if (this.#frameNavigatedWithinDocumentTarget) { this.#frameNavigatedWithinDocumentTarget.off( FrameEvent.FrameNavigatedWithinDocument, @@ -347,7 +345,7 @@ class PageEventSubscriber { } } - #onIssueAdded = (inspectorIssue: Issue) => { + #onIssueAdded = (inspectorIssue: Issue): void => { try { // @ts-expect-error The types are missmatched but they // are coming from CDP @@ -401,7 +399,7 @@ export class NetworkCollector extends PageCollector { ) { super(page, listeners, maxRequestsPerNavigation); } - override splitAfterNavigation() { + override splitAfterNavigation(): void { const requests = this.storage[0]; const lastRequestIdx = requests.findLastIndex(request => { diff --git a/src/collectors/ServiceWorkerCollector.ts b/src/collectors/ServiceWorkerCollector.ts index 99c887f97..6bad639b9 100644 --- a/src/collectors/ServiceWorkerCollector.ts +++ b/src/collectors/ServiceWorkerCollector.ts @@ -33,7 +33,7 @@ export class ServiceWorkerSubscriber { this.#callback = callback; } - async subscribe() { + async subscribe(): Promise { this.#session = await this.#target.createCDPSession(); await this.#session.send('Runtime.enable'); this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown); @@ -44,18 +44,18 @@ export class ServiceWorkerSubscriber { } } - async unsubscribe() { + async unsubscribe(): Promise { if (this.#worker) { this.#worker.off('console', this.#onConsole); } await this.#session?.detach(); } - #onConsole = (message: ConsoleMessage) => { + #onConsole = (message: ConsoleMessage): void => { this.#callback(message); }; - #onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => { + #onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent): void => { const url = this.#target.url(); const extensionId = extractExtensionId(url); @@ -81,7 +81,7 @@ export class ServiceWorkerConsoleCollector { this.#maxLogs = maxLogs; } - async init(workers: McpWorker[]) { + async init(workers: McpWorker[]): Promise { if (!this.#browser) { return; } @@ -93,7 +93,7 @@ export class ServiceWorkerConsoleCollector { } } - dispose() { + dispose(): void { if (!this.#browser) { return; } @@ -109,7 +109,7 @@ export class ServiceWorkerConsoleCollector { this.#serviceWorkerSubscribers.clear(); } - #onTargetCreated = async (target: Target) => { + #onTargetCreated = async (target: Target): Promise => { if (this.#serviceWorkerSubscribers.has(target)) { return; } @@ -141,7 +141,7 @@ export class ServiceWorkerConsoleCollector { } }; - #onTargetDestroyed = async (target: Target) => { + #onTargetDestroyed = async (target: Target): Promise => { const subscriber = this.#serviceWorkerSubscribers.get(target); if (subscriber) { this.#serviceWorkerSubscribers.delete(target); @@ -155,7 +155,7 @@ export class ServiceWorkerConsoleCollector { } }; - addLog(extensionId: string, log: ConsoleMessage | UncaughtError) { + addLog(extensionId: string, log: ConsoleMessage | UncaughtError): void { const logs = this.#storage.get(extensionId) ?? []; const withId = log as WithSymbolId; withId[stableIdSymbol] = this.#idGenerator(); @@ -198,7 +198,7 @@ export class ServiceWorkerConsoleCollector { return logs.find(filter); } - clearLogs(extensionId: string) { + clearLogs(extensionId: string): void { this.#storage.delete(extensionId); } } @@ -216,7 +216,7 @@ function extractExtensionId(origin: string): string | null { return slashIndex === -1 ? pathPart : pathPart.substring(0, slashIndex); } -function isExtensionOrigin(origin: string) { +function isExtensionOrigin(origin: string): boolean { return origin.startsWith(CHROME_EXTENSION_PREFIX); } diff --git a/src/config/ConfigParser.ts b/src/config/ConfigParser.ts index 9ddfc5d36..355b356ed 100644 --- a/src/config/ConfigParser.ts +++ b/src/config/ConfigParser.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {Argv} from 'yargs'; import type {YargsOptions, InferredOptionTypes} from '../third_party/index.js'; import {yargs, hideBin} from '../third_party/index.js'; @@ -32,15 +33,17 @@ function getErrorMessage(err: unknown): string { function stripYargsPositionalArgs( parsed: T, ): Omit { - const {_: _positionals, $0: _scriptName, ...rest} = parsed; - return rest; + const copy = {...parsed}; + delete copy._; + delete copy.$0; + return copy; } export type ParsedArguments = InferredOptionTypes; export class ConfigParser { #configPath?: string; - public readonly configLocator: ConfigLocator; + readonly configLocator: ConfigLocator; /** * @param configLocator Finds the config file when `--config` is not passed. @@ -55,7 +58,9 @@ export class ConfigParser { this.configLocator = new ConfigLocator(); } - buildCliParser(options: Record = mcpOptions) { + buildCliParser( + options: Record = mcpOptions, + ): Argv>> { const yargsInstance = yargs(hideBin(this.argv)); return yargsInstance .scriptName('npx chrome-devtools-mcp@latest') diff --git a/src/config/cli-options.ts b/src/config/cli-options.ts index b7b030de5..85d7c52a5 100644 --- a/src/config/cli-options.ts +++ b/src/config/cli-options.ts @@ -5,7 +5,7 @@ */ /** - * @fileoverview + * @file * WARNING: This file is auto-generated by 'npm run cli:generate'. * Do not edit this file manually. */ diff --git a/src/config/mcp-options.ts b/src/config/mcp-options.ts index 3fe278868..452c94f5a 100644 --- a/src/config/mcp-options.ts +++ b/src/config/mcp-options.ts @@ -8,13 +8,13 @@ import type {YargsOptions} from '../third_party/index.js'; import os from 'node:os'; import path from 'node:path'; -export const DEFAULT_FILESYSTEM_ROOT = [os.tmpdir()]; - import {getCategoryOptions} from './category-options.js'; import {getBrowserOptions} from './browser-options.js'; import {puppeteerOptions} from './puppeteer-options.js'; import {toolOptions} from './tool-options.js'; +export const DEFAULT_FILESYSTEM_ROOT = [os.tmpdir()]; + export const mcpOptions = { ...getCategoryOptions(), ...getBrowserOptions(), diff --git a/src/daemon/client.ts b/src/daemon/client.ts index 35c8bc33c..ad5de0c07 100644 --- a/src/daemon/client.ts +++ b/src/daemon/client.ts @@ -31,7 +31,7 @@ const FILE_TIMEOUT = 10_000; const READY_CHECK_INTERVAL = 100; const READY_CHECK_COMMAND_TIMEOUT = 1_000; -function delay(ms: number) { +function delay(ms: number): Promise { return new Promise(resolve => { setTimeout(resolve, ms); }); @@ -40,8 +40,8 @@ function delay(ms: number) { /** * Waits for a file to be created and populated (removed = false) or removed (removed = true). */ -async function waitForFile(filePath: string, removed = false) { - const check = () => { +async function waitForFile(filePath: string, removed = false): Promise { + const check = (): boolean => { const exists = fs.existsSync(filePath); if (removed) { return !exists; @@ -76,7 +76,7 @@ async function waitForFile(filePath: string, removed = false) { ); } -async function waitForDaemonReady(sessionId: string) { +async function waitForDaemonReady(sessionId: string): Promise { const deadline = Date.now() + FILE_TIMEOUT; let lastError: unknown; @@ -107,7 +107,10 @@ async function waitForDaemonReady(sessionId: string) { ); } -export async function startDaemon(mcpArgs: string[] = [], sessionId: string) { +export async function startDaemon( + mcpArgs: string[] = [], + sessionId: string, +): Promise { if (isDaemonRunning(sessionId)) { logger?.('Daemon is already running'); await waitForDaemonReady(sessionId); @@ -166,7 +169,7 @@ export async function sendCommand( path: socketPath, }); - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const timer = setTimeout(() => { socket.destroy(); reject(new Error('Timeout waiting for daemon response')); @@ -194,7 +197,7 @@ export async function sendCommand( }); } -export async function stopDaemon(sessionId: string) { +export async function stopDaemon(sessionId: string): Promise { if (!isDaemonRunning(sessionId)) { logger?.('Daemon is not running'); return; @@ -206,7 +209,7 @@ export async function stopDaemon(sessionId: string) { await sendCommand({method: 'stop'}, sessionId); try { - await waitForFile(pidFilePath, /*removed=*/ true); + await waitForFile(pidFilePath, /* removed=*/ true); } catch (error) { if (pid) { try { diff --git a/src/daemon/daemon.ts b/src/daemon/daemon.ts index 9b7dc428e..3ed0b6d37 100644 --- a/src/daemon/daemon.ts +++ b/src/daemon/daemon.ts @@ -138,7 +138,7 @@ const mcpServerArgs = process.argv.slice(2); let mcpServer: McpServer | null = null; let server: Server | null = null; -async function setupMCPServer() { +async function setupMCPServer(): Promise { logger?.(`Starting Chrome DevTools MCP Server v${VERSION}`); const configParser = new ConfigParser(VERSION); const args = configParser.parse(); @@ -155,7 +155,12 @@ async function setupMCPServer() { void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, mcpOptions)); } -async function handleRequest(msg: DaemonMessage) { +async function handleRequest(msg: DaemonMessage): Promise<{ + success: boolean; + result?: string; + message?: string; + error?: string; +}> { try { if (msg.method === 'invoke_tool') { await started; @@ -170,7 +175,8 @@ async function handleRequest(msg: DaemonMessage) { success: true, result: JSON.stringify(result), }; - } else if (msg.method === 'stop') { + } + if (msg.method === 'stop') { // Ensure we are not interrupting in-progress starting. await started; // Trigger cleanup asynchronously. @@ -181,7 +187,8 @@ async function handleRequest(msg: DaemonMessage) { success: true, message: 'stopping', }; - } else if (msg.method === 'status') { + } + if (msg.method === 'status') { await started; const statusResult: DaemonStatusResult = { pid: process.pid, @@ -210,7 +217,7 @@ async function handleRequest(msg: DaemonMessage) { } } -async function startSocketServer() { +async function startSocketServer(): Promise { // Remove existing socket file if it exists (only on non-Windows) if (!IS_WINDOWS) { try { @@ -261,7 +268,7 @@ async function startSocketServer() { let isCleaningUp = false; -async function cleanup(exitCode = 0) { +async function cleanup(exitCode = 0): Promise { if (isCleaningUp) { return; } @@ -273,8 +280,8 @@ async function cleanup(exitCode = 0) { } catch (error) { logger?.('Error closing MCP server:', error); } - if (server) { - const activeServer = server; + const activeServer = server; + if (activeServer) { await new Promise(resolve => { activeServer.close(() => resolve()); }); diff --git a/src/daemon/utils.ts b/src/daemon/utils.ts index 2aff83634..217282c35 100644 --- a/src/daemon/utils.ts +++ b/src/daemon/utils.ts @@ -67,13 +67,13 @@ export function getRuntimeHome(sessionId: string): string { export const IS_WINDOWS = os.platform() === 'win32'; -export function getPidFilePath(sessionId: string) { +export function getPidFilePath(sessionId: string): string { assertValidSessionId(sessionId); const runtimeDir = getRuntimeHome(sessionId); return path.join(runtimeDir, 'daemon.pid'); } -export function getDaemonPid(sessionId: string) { +export function getDaemonPid(sessionId: string): number | null { assertValidSessionId(sessionId); try { const pidFile = getPidFilePath(sessionId); diff --git a/src/devtools/DevtoolsUtils.ts b/src/devtools/DevtoolsUtils.ts index 916242b99..f991c6106 100644 --- a/src/devtools/DevtoolsUtils.ts +++ b/src/devtools/DevtoolsUtils.ts @@ -36,7 +36,7 @@ export function overrideDevToolsGlobals({ // DevTools CDP errors can get noisy. DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true; - const noopAgentCommand = () => { + const noopAgentCommand = (): Promise<{getError: () => undefined}> => { return Promise.resolve({ getError: () => undefined, }); @@ -320,7 +320,7 @@ export class SymbolizedError { opts.targetId, ); if (details) { - return SymbolizedError.fromDetails({ + return await SymbolizedError.fromDetails({ details, devTools: opts.devTools, targetId: opts.targetId, @@ -400,7 +400,7 @@ export class SymbolizedError { message: string, stackTrace?: DevTools.StackTrace.StackTrace.StackTrace, cause?: SymbolizedError, - ) { + ): SymbolizedError { return new SymbolizedError(message, stackTrace, cause); } } @@ -415,7 +415,7 @@ export async function createStackTraceForConsoleMessage( }; const rawStackTrace = message._rawStackTrace(); if (rawStackTrace) { - return createStackTrace(devTools, rawStackTrace, message._targetId()); + return await createStackTrace(devTools, rawStackTrace, message._targetId()); } return undefined; } @@ -465,7 +465,7 @@ export async function createStackTrace( DevTools.DebuggerWorkspaceBinding, ); // DevTools uses branded types for ScriptId and others. Casting the puppeteer protocol type to the DevTools protocol type is safe. - return binding.createStackTraceFromProtocolRuntime( + return await binding.createStackTraceFromProtocolRuntime( rawStackTrace as Parameters< DevTools.DebuggerWorkspaceBinding['createStackTraceFromProtocolRuntime'] >[0], @@ -478,7 +478,7 @@ async function waitForScript( model: DevTools.DebuggerModel, scriptId: string, signal: AbortSignal, -) { +): Promise>> { while (true) { if (signal.aborted) { throw signal.reason; diff --git a/src/devtools/McpHostBindingAdapter.ts b/src/devtools/McpHostBindingAdapter.ts index c79662204..e78047d73 100644 --- a/src/devtools/McpHostBindingAdapter.ts +++ b/src/devtools/McpHostBindingAdapter.ts @@ -4,8 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable @typescript-eslint/no-empty-function */ - import {DevTools} from '../third_party/index.js'; /** diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index aada6a3b1..906b17b83 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -323,7 +323,9 @@ export class GroupedConsoleFormatter extends ConsoleFormatter { } } -function convertConsoleMessageConciseToString(msg: ConsoleMessageConcise) { +function convertConsoleMessageConciseToString( + msg: ConsoleMessageConcise, +): string { const countSuffix = msg.count && msg.count > 1 ? ` [${msg.count} times]` : ''; const messageLine = `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)${countSuffix}`; if (!msg.stackTrace) { @@ -338,7 +340,7 @@ function convertConsoleMessageConciseToString(msg: ConsoleMessageConcise) { function convertConsoleMessageConciseDetailedToString( msg: ConsoleMessageDetailed, -) { +): string { const result = [ `ID: ${msg.id}`, `Message: ${msg.type}> ${msg.text}`, @@ -364,7 +366,7 @@ function formatArgs(msg: ConsoleMessageDetailed): string { return result.join('\n'); } -function formatArg(arg: unknown, formatter: {isIgnored: IgnoreCheck}) { +function formatArg(arg: unknown, formatter: {isIgnored: IgnoreCheck}): string { if (arg instanceof SymbolizedError) { return [ arg.message, diff --git a/src/formatters/CssFormatter.ts b/src/formatters/CssFormatter.ts index 707071e01..2f4e7feb0 100644 --- a/src/formatters/CssFormatter.ts +++ b/src/formatters/CssFormatter.ts @@ -354,8 +354,9 @@ function createContainerQueryAncestor( } /** - * Collects enclosing ancestor rules (@media, @container, @supports, @layer, - * @scope, @starting-style, @navigation, and CSS nesting) for a style rule. + * Collects enclosing ancestor rules (`@media`, `@container`, `@supports`, + * `@layer`, `@scope`, `@starting-style`, `@navigation`, and CSS nesting) for a + * style rule. */ function collectAncestorRules( rule: DevTools.CSSRule.CSSStyleRule, diff --git a/src/formatters/HeapSnapshotFormatter.ts b/src/formatters/HeapSnapshotFormatter.ts index 459a808ba..952d603be 100644 --- a/src/formatters/HeapSnapshotFormatter.ts +++ b/src/formatters/HeapSnapshotFormatter.ts @@ -181,7 +181,7 @@ export class HeapSnapshotFormatter { function formatEdge( edge: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingEdge, depth: number, - ) { + ): void { const indent = ' '.repeat(depth); lines.push( `${indent}<- @${edge.nodeId} ${formatName(edge.nodeName, options)} via ${edge.edgeType} ${formatName(edge.edgeName, options)} (distance: ${edge.distance})`, diff --git a/src/formatters/IssueFormatter.ts b/src/formatters/IssueFormatter.ts index e8a662847..9d6e18536 100644 --- a/src/formatters/IssueFormatter.ts +++ b/src/formatters/IssueFormatter.ts @@ -146,7 +146,7 @@ export class IssueFormatter { delete data.frameId; affectedResources.push({ uid, - data: data, + data, request, }); } @@ -158,8 +158,9 @@ export class IssueFormatter { } #getParsedDescription(): {title?: string; description?: string} { - if (parsedDescriptionCache.has(this.#issue)) { - return parsedDescriptionCache.get(this.#issue)!; + const cached = parsedDescriptionCache.get(this.#issue); + if (cached) { + return cached; } const markdownDescription = this.#issue.getDescription(); diff --git a/src/formatters/NetworkFormatter.ts b/src/formatters/NetworkFormatter.ts index 3b7510747..71a86c648 100644 --- a/src/formatters/NetworkFormatter.ts +++ b/src/formatters/NetworkFormatter.ts @@ -101,15 +101,10 @@ export class NetworkFormatter { } else { this.#requestBody = requestBodyNotAvailableMessage; } + } else if (data) { + this.#requestBody = getSizeLimitedString(data, BODY_CONTEXT_SIZE_LIMIT); } else { - if (data) { - this.#requestBody = getSizeLimitedString( - data, - BODY_CONTEXT_SIZE_LIMIT, - ); - } else { - this.#requestBody = requestBodyNotAvailableMessage; - } + this.#requestBody = requestBodyNotAvailableMessage; } } @@ -254,7 +249,7 @@ export class NetworkFormatter { } } -function getSizeLimitedString(text: string, sizeLimit: number) { +function getSizeLimitedString(text: string, sizeLimit: number): string { if (text.length > sizeLimit) { return text.substring(0, sizeLimit) + '... '; } diff --git a/src/index.ts b/src/index.ts index 1fb88fd57..b2cf5ef22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -354,7 +354,7 @@ export async function createMcpServer( return {server: server.server}; } -export const logDisclaimers = (args: ParsedArguments) => { +export const logDisclaimers = (args: ParsedArguments): void => { console.error( `chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect, debug, and modify any data in the browser or DevTools. diff --git a/src/processors/ChunkedTraceParser.ts b/src/processors/ChunkedTraceParser.ts index b74bdd848..e3d600890 100644 --- a/src/processors/ChunkedTraceParser.ts +++ b/src/processors/ChunkedTraceParser.ts @@ -5,7 +5,7 @@ */ /** - * @fileoverview Provides memory-efficient chunked parsing for Chrome DevTools trace files. + * @file Provides memory-efficient chunked parsing for Chrome DevTools trace files. * Large performance traces can exceed V8 string length limits and cause heap exhaustion. * This parser scans raw byte buffers, finds event boundaries, and parses events in batches. */ @@ -31,14 +31,14 @@ export interface ParseTraceBufferOptions { * Lower values reduce memory spikes during string decoding. * Higher values decrease the total number of JSON.parse calls. * Non-positive or non-finite values fall back to the default. - * @defaultValue 10_000 + * @default 10_000 */ eventsPerBatch?: number; /** * The maximum byte size of raw trace event data to decode in a single batch. * Flushes batches before V8 string length limits are approached. * Non-positive or non-finite values fall back to the default. - * @defaultValue 33_554_432 (32 MB) + * @default 33_554_432 (32 MB) */ maxBatchBytes?: number; } @@ -68,8 +68,8 @@ const BYTE_COMMA = 0x2c; /** * Advances past any RFC 8259 JSON whitespace bytes. * - * @param buffer - The trace byte buffer to inspect. - * @param start - The buffer index where scanning begins. + * @param buffer The trace byte buffer to inspect. + * @param start The buffer index where scanning begins. * @returns The index of the first non-whitespace byte, or the buffer length if the buffer ends. */ function skipWhitespace( @@ -90,8 +90,8 @@ function skipWhitespace( /** * Scans past a JSON string starting at the opening quotation mark. * - * @param buffer - The trace byte buffer to scan. - * @param startQuotePos - The buffer index of the opening quotation mark. + * @param buffer The trace byte buffer to scan. + * @param startQuotePos The buffer index of the opening quotation mark. * @returns The index immediately following the closing quotation mark. * @throws {SyntaxError} If the string is unterminated before the end of the buffer. */ @@ -121,8 +121,8 @@ function skipString( * Scans past a complete JSON value starting at the specified buffer position. * Skips strings, compound objects, arrays, and primitive values without parsing them into memory. * - * @param buffer - The trace byte buffer to scan. - * @param startPos - The buffer index where the value begins. + * @param buffer The trace byte buffer to scan. + * @param startPos The buffer index where the value begins. * @returns The index immediately following the skipped JSON value. * @throws {SyntaxError} If the value contains unbalanced delimiters, unterminated strings, or empty primitive tokens. */ @@ -194,9 +194,9 @@ function skipValue( /** * Scans trace events from a JSON array in bounded batches to limit memory consumption. * - * @param buffer - The raw byte buffer containing the events array. - * @param startPos - The index immediately after the opening array bracket. - * @param config - Configuration settings controlling batch sizes and text decoding. + * @param buffer The raw byte buffer containing the events array. + * @param startPos The index immediately after the opening array bracket. + * @param config Configuration settings controlling batch sizes and text decoding. * @returns An object containing the parsed events and the ending buffer position. * @throws {SyntaxError} If the event array contains malformed JSON, unbalanced delimiters, or unexpected tokens. */ @@ -330,8 +330,8 @@ function parseEventsArray( * Supports both root array format ([...]) and object container format ({"traceEvents": [...]}). * Parses events in chunks to prevent V8 string length and memory exhaustion errors. * - * @param buffer - The raw trace buffer to parse. - * @param options - Optional configuration settings for batch processing. + * @param buffer The raw trace buffer to parse. + * @param options Optional configuration settings for batch processing. * @returns A {@link ParsedTraceBuffer} containing parsed trace events and optional file metadata, or an object with an empty events array if the buffer is empty or contains only whitespace. * @throws {Error} If the buffer contains non-whitespace data that does not begin with a valid JSON array or object. * @throws {SyntaxError} If the underlying JSON syntax within batches or metadata is invalid. diff --git a/src/processors/HeapSnapshotManager.ts b/src/processors/HeapSnapshotManager.ts index e35d614bb..6c6ab5021 100644 --- a/src/processors/HeapSnapshotManager.ts +++ b/src/processors/HeapSnapshotManager.ts @@ -337,7 +337,12 @@ export class HeapSnapshotManager { }; } - #getCachedSnapshot(filePath: string) { + #getCachedSnapshot(filePath: string): { + snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy; + worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy; + idToClassKey: string[]; + classKeyToId: Map; + } { const absolutePath = path.resolve(filePath); const cached = this.#snapshots.get(absolutePath); if (!cached) { diff --git a/src/processors/PerformanceTrace.ts b/src/processors/PerformanceTrace.ts index 2c61fd2d0..d4101e061 100644 --- a/src/processors/PerformanceTrace.ts +++ b/src/processors/PerformanceTrace.ts @@ -21,7 +21,7 @@ export interface TraceResult { /** * Type guard that verifies if an operation returned a valid TraceResult. * - * @param x - The result or error object to inspect. + * @param x The result or error object to inspect. * @returns True if the object is a TraceResult; otherwise false. */ export function traceResultIsSuccess( @@ -46,8 +46,8 @@ export interface TraceParseError { * allocation limits. A new trace engine model is created per call to ensure session * isolation and prevent memory retention. * - * @param buffer - Raw binary trace data representing trace events and metadata. - * @param metadata - Optional throttling configurations applied during recording; overrides embedded file metadata when defined. + * @param buffer Raw binary trace data representing trace events and metadata. + * @param metadata Optional throttling configurations applied during recording; overrides embedded file metadata when defined. * @returns A promise resolving to a {@link TraceResult} with parsed traces and insights, or a {@link TraceParseError} on failure. */ export async function parseRawTraceBuffer( @@ -119,8 +119,8 @@ ${DevTools.PerformanceTraceFormatter.networkDataFormatDescription}`; /** * Generates a Markdown summary of main thread activity and network metrics from a parsed trace. * - * @param result - The parsed trace result to summarize. - * @param deviceScope - Optional CrUX device scope to filter field data. + * @param result The parsed trace result to summarize. + * @param deviceScope Optional CrUX device scope to filter field data. * @returns Formatted Markdown text describing performance findings. */ export function getTraceSummary( @@ -147,10 +147,10 @@ export type InsightOutput = {output: string} | {error: string}; /** * Formats a specific performance insight from a parsed trace for display. * - * @param result - The parsed trace result containing computed insight sets. - * @param insightSetId - The identifier of the target insight set. - * @param insightName - The name of the insight model to extract. - * @param deviceScope - Optional CrUX device scope to contextualize metrics. + * @param result The parsed trace result containing computed insight sets. + * @param insightSetId The identifier of the target insight set. + * @param insightName The name of the insight model to extract. + * @param deviceScope Optional CrUX device scope to contextualize metrics. * @returns An object containing the formatted insight output text or an error message. */ export function getInsightOutput( diff --git a/src/telemetry/ClearcutLogger.ts b/src/telemetry/ClearcutLogger.ts index 13c16ec8d..b3b0a1cdc 100644 --- a/src/telemetry/ClearcutLogger.ts +++ b/src/telemetry/ClearcutLogger.ts @@ -217,7 +217,7 @@ export class ClearcutLogger { this.#watchdog.send({ type: WatchdogMessageType.LOG_EVENT, payload: this.#addMcpClient({ - tool_invocation: tool_invocation, + tool_invocation, }), }); } diff --git a/src/telemetry/transformation.ts b/src/telemetry/transformation.ts index c925c9ad7..98f0bd9d4 100644 --- a/src/telemetry/transformation.ts +++ b/src/telemetry/transformation.ts @@ -211,31 +211,34 @@ function transformValue( ): LoggedToolCallArgValue { if (zodType === 'ZodString') { return bucketize((value as string).length); - } else if (zodType === 'ZodArray') { + } + if (zodType === 'ZodArray') { return (value as unknown[]).length; - } else { - return value as LoggedToolCallArgValue; } + return value as LoggedToolCallArgValue; } function hasEquivalentType(zodType: ZodType, value: unknown): boolean { if (zodType === 'ZodString') { return typeof value === 'string'; - } else if (zodType === 'ZodArray') { + } + if (zodType === 'ZodArray') { return Array.isArray(value); - } else if (zodType === 'ZodNumber') { + } + if (zodType === 'ZodNumber') { return typeof value === 'number'; - } else if (zodType === 'ZodBoolean') { + } + if (zodType === 'ZodBoolean') { return typeof value === 'boolean'; - } else if (zodType === 'ZodEnum') { + } + if (zodType === 'ZodEnum') { return ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ); - } else { - return false; } + return false; } export function sanitizeParams( diff --git a/src/telemetry/watchdog/main.ts b/src/telemetry/watchdog/main.ts index c2237cb1f..2407c126d 100644 --- a/src/telemetry/watchdog/main.ts +++ b/src/telemetry/watchdog/main.ts @@ -75,7 +75,7 @@ function parseWatchdogArgs(): WatchdogArgs { }; } -function main() { +function main(): void { const { parentPid, appVersion, @@ -90,7 +90,7 @@ function main() { logStream = saveLogsToFile(logFile); } - const exit = (code: number) => { + const exit = (code: number): void => { if (!logStream) { process.exit(code); } @@ -116,14 +116,14 @@ function main() { const sender = new ClearcutSender({ appVersion, - osType: osType, + osType, clearcutEndpoint, forceFlushIntervalMs: clearcutForceFlushIntervalMs, includePidHeader: clearcutIncludePidHeader, }); let isShuttingDown = false; - function onParentDeath(reason: string) { + function onParentDeath(reason: string): void { if (isShuttingDown) { return; } diff --git a/src/third_party/index.ts b/src/third_party/index.ts index 0a2aa2274..208151051 100644 --- a/src/third_party/index.ts +++ b/src/third_party/index.ts @@ -12,6 +12,12 @@ import type {Flags, OutputMode, Result, RunnerResult} from 'lighthouse'; import type {Page} from 'puppeteer-core'; import {z as zod} from 'zod'; +import { + snapshot as snapshotImpl, + navigation as navigationImpl, + generateReport as generateReportImpl, +} from './lighthouse-devtools-mcp-bundle.js'; + export type {Flags, Result, RunnerResult, OutputMode}; export type {Options as YargsOptions, InferredOptionTypes} from 'yargs'; @@ -90,12 +96,6 @@ export async function getGcfEncode(): Promise<(val: unknown) => string> { return encodeGeneric; } -import { - snapshot as snapshotImpl, - navigation as navigationImpl, - generateReport as generateReportImpl, -} from './lighthouse-devtools-mcp-bundle.js'; - export const lighthouseRunner = { snapshot: snapshotImpl as ( page: Page, diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 73c502a93..232f58e4e 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -42,9 +42,9 @@ import type { TextSnapshotNode, GeolocationOptions, CD4ACommentThread, + PaginationOptions, } from '../types.js'; import type {McpWorker} from '../McpWorker.js'; -import type {PaginationOptions} from '../types.js'; import type { WaitForEventsResult, DialogAction, @@ -540,13 +540,15 @@ export function viewportTransform(arg: string | undefined): width, height, deviceScaleFactor: dpr, - isMobile: isMobile, - isLandscape: isLandscape, - hasTouch: hasTouch, + isMobile, + isLandscape, + hasTouch, }; } -export function geolocationTransform(arg: string | undefined) { +export function geolocationTransform( + arg: string | undefined, +): {latitude: number; longitude: number} | undefined { if (!arg) { return undefined; } diff --git a/src/tools/input.ts b/src/tools/input.ts index 75169c16b..4276ff202 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -38,7 +38,11 @@ const submitKeySchema = zod * The open dialog is already reported in the response, so treat this as an * interruption instead of a failure. */ -function handleActionError(error: unknown, uid: string, page: ContextPage) { +function handleActionError( + error: unknown, + uid: string, + page: ContextPage, +): void { if (page.getDialog()) { logger?.('action interrupted by a dialog', error); return; @@ -62,7 +66,7 @@ function handleActionError(error: unknown, uid: string, page: ContextPage) { async function selectNativeSelectOption( handle: ElementHandle, signal: AbortSignal, -) { +): Promise { using selectHandle = await handle.evaluateHandle(node => { if (!(node instanceof HTMLOptionElement)) { return null; @@ -243,7 +247,7 @@ function raceWithSignal( return Promise.reject(signal.reason); } return new Promise((resolve, reject) => { - const onAbort = () => { + const onAbort = (): void => { reject(signal.reason); }; signal.addEventListener('abort', onAbort, {once: true}); @@ -269,7 +273,7 @@ async function selectOption( aXNode: TextSnapshotNode, value: string, signal: AbortSignal, -) { +): Promise { let optionFound = false; for (const child of aXNode.children) { if (child.role === 'option' && child.name === value && child.value) { @@ -298,7 +302,7 @@ async function selectOption( } } -function hasOptionChildren(aXNode: TextSnapshotNode) { +function hasOptionChildren(aXNode: TextSnapshotNode): boolean { return aXNode.children.some(child => child.role === 'option'); } diff --git a/src/tools/screencast.ts b/src/tools/screencast.ts index 7c29acd6e..cba48b48f 100644 --- a/src/tools/screencast.ts +++ b/src/tools/screencast.ts @@ -88,7 +88,7 @@ export const startScreencast = definePageTool((args: ParsedArguments) => ({ try { recorder = await page.pptrPage.screencast({ path: resolvedPath, - format: format, + format, ffmpegPath: args?.experimentalFfmpegPath, fps: args?.experimentalScreencastFps, }); diff --git a/src/tools/script.ts b/src/tools/script.ts index 3e6e197cd..e9550e510 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -225,7 +225,7 @@ const performEvaluation = async ( args: Array>, response: Response, options: {filePath?: string; context: Context}, -) => { +): Promise => { let result: string | undefined; if (format === 'function') { const functionSource = source.trimEnd().replace(/;$/, ''); diff --git a/src/tools/slim/tools.ts b/src/tools/slim/tools.ts index 6cab86529..19a2c77cf 100644 --- a/src/tools/slim/tools.ts +++ b/src/tools/slim/tools.ts @@ -64,7 +64,7 @@ export const navigate = definePageTool((args: ParsedArguments) => { timeout: 30_000, }; - const dialogHandler = (dialog: Dialog) => { + const dialogHandler = (dialog: Dialog): void => { if (dialog.type() === 'beforeunload') { response.appendResponseLine(`Accepted a beforeunload dialog.`); void dialog.accept(); diff --git a/src/tools/thirdPartyDeveloper.ts b/src/tools/thirdPartyDeveloper.ts index 79af6b9fd..83c16bf30 100644 --- a/src/tools/thirdPartyDeveloper.ts +++ b/src/tools/thirdPartyDeveloper.ts @@ -25,6 +25,7 @@ export type ToolGroups = Array>; declare global { interface Window { + // eslint-disable-next-line @typescript-eslint/naming-convention __dtmcp?: { toolGroups?: Array< ToolGroup< diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 32c342127..40b1cb831 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -29,7 +29,9 @@ import * as thirdPartyDeveloperTools from './thirdPartyDeveloper.js'; import type {DefinedPageTool, ToolDefinition} from './ToolDefinition.js'; import * as webmcpTools from './webmcp.js'; -export const createTools = (args: ParsedArguments) => { +export const createTools = ( + args: ParsedArguments, +): Array => { const rawTools = [ ...Object.values(commentsTools), ...Object.values(consoleTools), diff --git a/src/types.ts b/src/types.ts index 16e4e8d77..b91ae7926 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,7 +68,9 @@ declare global { universe?: { cd4aBridge?: CD4ABridge | null; }; + // eslint-disable-next-line @typescript-eslint/naming-convention __onDevToolsCommentEvent?: () => void; + // eslint-disable-next-line @typescript-eslint/naming-convention __onDevToolsCommentListener?: () => void; } } diff --git a/src/utils/WaitForHelper.ts b/src/utils/WaitForHelper.ts index 557030591..03a1821d2 100644 --- a/src/utils/WaitForHelper.ts +++ b/src/utils/WaitForHelper.ts @@ -4,8 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {Page, Protocol, CdpPage, Dialog} from '../third_party/index.js'; -import type {PredefinedNetworkConditions} from '../third_party/index.js'; +import type { + Page, + Protocol, + CdpPage, + Dialog, + PredefinedNetworkConditions, +} from '../third_party/index.js'; import {logger} from './logger.js'; export type DialogAction = 'accept' | 'dismiss' | string; @@ -48,7 +53,7 @@ export class WaitForHelper { using stableDomObserver = await Promise.race([ this.#page.evaluateHandle(timeout => { let timeoutId: ReturnType; - function callback() { + function callback(): void { clearTimeout(timeoutId); timeoutId = setTimeout(() => { domObserver.resolver.resolve(); @@ -89,7 +94,7 @@ export class WaitForHelper { } }); - return Promise.race([ + return await Promise.race([ stableDomObserver.evaluate(async observer => { return await observer.resolver.promise; }), @@ -127,7 +132,7 @@ export class WaitForHelper { } const dialogHandler = ( dialog: Pick, - ) => { + ): void => { this.#dialogDetected = true; let actionToTake: DialogAction | undefined; @@ -167,7 +172,7 @@ export class WaitForHelper { const navigationListener = ( event: Protocol.Page.FrameStartedNavigatingEvent, - ) => { + ): void => { if (event.frameId !== this.#page.mainFrame()._id) { return; } @@ -182,7 +187,7 @@ export class WaitForHelper { }; const requestedNavigationListener = ( event: Protocol.Page.FrameRequestedNavigationEvent, - ) => { + ): void => { if (event.frameId === this.#page.mainFrame()._id) { navigationStartedResolvers.resolve(true); } diff --git a/src/utils/bytes.ts b/src/utils/bytes.ts index 4e5ad4151..5a194e54c 100644 --- a/src/utils/bytes.ts +++ b/src/utils/bytes.ts @@ -115,7 +115,9 @@ export function parseByteSizeRange(value: string): ByteSizeRange { return {min, max}; } -export function byteSizeRangeSchema(description: string) { +export function byteSizeRangeSchema( + description: string, +): zod.ZodType { return zod .string() .transform((value, context) => { diff --git a/src/utils/check-for-updates.ts b/src/utils/check-for-updates.ts index f88ec0c77..538153d07 100644 --- a/src/utils/check-for-updates.ts +++ b/src/utils/check-for-updates.ts @@ -19,12 +19,12 @@ import {VERSION} from '../version.js'; */ let isChecking = false; -/** @internal Reset flag for tests only. */ -export function resetUpdateCheckFlagForTesting() { +/** @internal */ +export function resetUpdateCheckFlagForTesting(): void { isChecking = false; } -export async function checkForUpdates(message: string) { +export async function checkForUpdates(message: string): Promise { if (isChecking || process.env['CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS']) { return; } diff --git a/src/utils/errorHandling.ts b/src/utils/errorHandling.ts index 5184ca4ba..cc697a811 100644 --- a/src/utils/errorHandling.ts +++ b/src/utils/errorHandling.ts @@ -7,7 +7,7 @@ import process from 'node:process'; import {logger} from './logger.js'; -export function setupUnhandledRejectionHandler(onCrash: () => void) { +export function setupUnhandledRejectionHandler(onCrash: () => void): void { process.on('unhandledRejection', (reason, promise) => { logger?.('Unhandled promise rejection:', promise, reason); console.error('Unhandled promise rejection:', reason); diff --git a/src/utils/files.ts b/src/utils/files.ts index 74b371d26..086e6bb8f 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -8,7 +8,7 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -export async function getTempFilePath(filename: string) { +export async function getTempFilePath(filename: string): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-')); const filepath = path.join(dir, filename); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 71a4fbff8..6d9edbc39 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -13,7 +13,7 @@ const mcpDebugNamespace = 'mcp:log'; let logFileStream: fs.WriteStream | undefined; -const _debugLog = util.debuglog(mcpDebugNamespace); +const debugLog = util.debuglog(mcpDebugNamespace); export function saveLogsToFile(fileName: string): fs.WriteStream { const logFile = fs.createWriteStream(fileName, {flags: 'a+'}); @@ -57,27 +57,28 @@ export const logger: Logger = (...args: unknown[]) => { logFileStream.write( `${new Date().toISOString()} ${mcpDebugNamespace} ${util.format(...args)}\n`, ); - } else if (_debugLog.enabled) { - _debugLog('%s %s', new Date().toISOString(), util.format(...args)); + } else if (debugLog.enabled) { + debugLog('%s %s', new Date().toISOString(), util.format(...args)); } }; -export const puppeteerLogger = (prefix: string) => { +export const puppeteerLogger = (prefix: string): Logger | undefined => { const dbg = util.debuglog(prefix); if (!dbg.enabled) { return; } - if (logFileStream) { - return (...args: unknown[]) => { - logFileStream!.write( + const stream = logFileStream; + if (stream) { + return (...args: unknown[]): void => { + stream.write( `${new Date().toISOString()} ${prefix} ${util.format(...args)}\n`, ); }; } - return (...args: unknown[]) => { + return (...args: unknown[]): void => { dbg('%s %s', new Date().toISOString(), util.format(...args)); }; }; diff --git a/tests/McpPage.test.ts b/tests/McpPage.test.ts index 0bfe47ec1..6263c7257 100644 --- a/tests/McpPage.test.ts +++ b/tests/McpPage.test.ts @@ -10,8 +10,7 @@ import {afterEach, describe, it} from 'node:test'; import sinon from 'sinon'; import type {TargetUniverse} from '../src/devtools/DevtoolsUtils.js'; -import {McpPage} from '../src/McpPage.js'; -import {replaceHtmlElementsWithUids} from '../src/McpPage.js'; +import {McpPage, replaceHtmlElementsWithUids} from '../src/McpPage.js'; import {DevTools, Locator} from '../src/third_party/index.js'; import type {JSONSchema7Definition} from '../src/third_party/index.js'; import {TextSnapshot} from '../src/TextSnapshot.js'; diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 34b266358..6f8493b92 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -529,6 +529,7 @@ describe('McpResponse', () => { }); }); page.evaluate(() => { + // eslint-disable-next-line no-console console.log('Hello from the test'); }); await consoleMessagePromise; @@ -859,7 +860,7 @@ describe('extensions', () => { response.resetResponseLineForTesting(); // Testing with extensions context.listExtensions = async () => - Promise.resolve( + await Promise.resolve( new Map([ [ 'id1', diff --git a/tests/ToolHandler.test.ts b/tests/ToolHandler.test.ts index 24677d34d..70723158e 100644 --- a/tests/ToolHandler.test.ts +++ b/tests/ToolHandler.test.ts @@ -17,7 +17,7 @@ import {McpContext} from '../src/McpContext.js'; import {McpPage} from '../src/McpPage.js'; import {McpResponse, type DataFormat} from '../src/McpResponse.js'; import {ClearcutLogger} from '../src/telemetry/ClearcutLogger.js'; -import {zod} from '../src/third_party/index.js'; +import {zod, Mutex} from '../src/third_party/index.js'; import {TOOL_CALL_TIMEOUT_MS, ToolHandler} from '../src/ToolHandler.js'; import {ToolCategory} from '../src/tools/categories.js'; import { @@ -30,7 +30,6 @@ import {evaluateScript} from '../src/tools/script.js'; import {createTools} from '../src/tools/tools.js'; import {createMockMcpContext} from './mocks.js'; import {getMockBrowser} from './utils.js'; -import {Mutex} from '../src/third_party/index.js'; describe('ToolHandler', () => { afterEach(() => { @@ -1378,7 +1377,7 @@ describe('ToolHandler', () => { blockedByDialog: false, verifyFilesSchema: {}, handler: async () => { - return new Promise(() => { + return await new Promise(() => { // Simulates a tool call awaiting a CDP response on a transport // that died silently: it never resolves or rejects on its own. }); diff --git a/tests/browser.test.ts b/tests/browser.test.ts index 510133e04..4585cc998 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -53,7 +53,7 @@ async function runWithRetry(fn: () => Promise) { await new Promise(r => setTimeout(r, 500)); } } - throw lastError; + throw lastError ?? new Error('Failed without an error'); } describe('browser', () => { diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 0702aff3e..ea21e98c5 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -9,11 +9,14 @@ import path from 'node:path'; import {describe, it} from 'node:test'; import {ConfigParser} from '../src/config/ConfigParser.js'; -import {getCliOptions, mcpOptions} from '../src/config/mcp-options.js'; +import { + getCliOptions, + mcpOptions, + DEFAULT_FILESYSTEM_ROOT, +} from '../src/config/mcp-options.js'; import {buildCommand} from '../src/config/cli-commands.js'; import {commands} from '../src/config/cli-options.js'; import {computeFlagUsage} from '../src/telemetry/flagUtils.js'; -import {DEFAULT_FILESYSTEM_ROOT} from '../src/config/mcp-options.js'; import {createTempFile} from './utils.js'; diff --git a/tests/formatters/ConsoleFormatter.test.ts b/tests/formatters/ConsoleFormatter.test.ts index e037cc02a..8d84e70ac 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -9,8 +9,11 @@ import {describe, it} from 'node:test'; import {SymbolizedError} from '../../src/devtools/DevtoolsUtils.js'; import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js'; import {UncaughtError} from '../../src/collectors/PageCollector.js'; -import type {ConsoleMessage, Protocol} from '../../src/third_party/index.js'; -import type {DevTools} from '../../src/third_party/index.js'; +import type { + ConsoleMessage, + Protocol, + DevTools, +} from '../../src/third_party/index.js'; interface MockConsoleMessage { type: () => string; diff --git a/tests/mocks.ts b/tests/mocks.ts index 421642896..c3b785990 100644 --- a/tests/mocks.ts +++ b/tests/mocks.ts @@ -585,7 +585,7 @@ export function synthesizeRuleTypes( ]; for (const [items, ruleType] of mappings) { if (items) { - for (const _ of items) { + for (let i = 0; i < items.length; i++) { ruleTypes.push(ruleType); } } diff --git a/tests/shutdown.test.ts b/tests/shutdown.test.ts index 496234f49..4abe3a673 100644 --- a/tests/shutdown.test.ts +++ b/tests/shutdown.test.ts @@ -165,7 +165,7 @@ async function setupServerWithRetry(): Promise { await new Promise(r => setTimeout(r, 500)); } } - throw lastError; + throw lastError ?? new Error('Failed without an error'); } describe('shutdown', () => { diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index 2ddb4110e..7355d591b 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -73,7 +73,7 @@ describe('pages', () => { await context.triggerExtensionAction(extensionId); - const _popupTarget = await context.browser.waitForTarget( + await context.browser.waitForTarget( t => t.type() === 'page' && t.url().includes('chrome-extension://'), ); diff --git a/tests/tools/thirdPartyDeveloper.test.ts b/tests/tools/thirdPartyDeveloper.test.ts index 7340d8180..73cf76f78 100644 --- a/tests/tools/thirdPartyDeveloper.test.ts +++ b/tests/tools/thirdPartyDeveloper.test.ts @@ -567,7 +567,7 @@ describe('thirdPartyDeveloperTools', () => { toolName: 'test-tool', params: JSON.stringify({element: {uid: 'some-uid'}}), }, - page: page, + page, }, response, context, @@ -847,7 +847,7 @@ describe('thirdPartyDeveloperTools', () => { toolName: 'test-tool', params: JSON.stringify({}), }, - page: page, + page, }, response, context, @@ -900,7 +900,7 @@ describe('thirdPartyDeveloperTools', () => { toolName: 'test-tool', params: JSON.stringify({}), }, - page: page, + page, }, response, context, @@ -954,7 +954,7 @@ describe('thirdPartyDeveloperTools', () => { toolName: 'test-tool', params: JSON.stringify({}), }, - page: page, + page, }, response, context, diff --git a/tests/utils.ts b/tests/utils.ts index dc75921ba..3ea02db32 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -435,7 +435,7 @@ export async function runCli( sessionId?: string, options?: {cwd?: string}, ): Promise<{status: number | null; stdout: string; stderr: string}> { - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const finalArgs = [...args]; if (sessionId) { finalArgs.push('--sessionId', sessionId);