guides/eslint-migration.md
This guide describes how to migrate all packages in the Cypress monorepo to the new unified ESLint configuration (@packages/eslint-config).
@cypress/eslint-plugin-dev.@typescript-eslint).During the migration period, the root package.json uses explicit lint-staged patterns to ensure each package gets the correct linting treatment:
npm/grep): Use yarn lint:fix which runs ESLint from the package directory with the correct configeslint --fix which runs from the root with the root config*.{js,jsx,ts,tsx,json,eslintrc,vue} patternThis configuration will be simplified once all packages are migrated to the unified ESLint setup.
For each package in the batch:
.eslintrc, .eslintrc.json, or .eslintrc.js in the package.@cypress/eslint-plugin-dev in package.json (if present).tslint.json and remove tslint dependencies from package.json.eslint.config.ts in the package root:
import baseConfig from '@packages/eslint-config'
export default baseConfig
@typescript-eslint).@packages/eslint-config to devDependencies (use a relative file path if not published to npm).@packages/eslint-config has ESLint as a peer dependency, add eslint: "^9.18.0" to devDependencies.lint-staged section to the package's package.json:
{
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "eslint --fix"
}
}
npx eslint . --ext .js,.ts,.tsx,.jsx --fix
tsconfig.json that works with the new ESLint config.npx tsc --noEmit to check for TypeScript compilation errors.chore(npm/grep): migrate to @packages/eslint-config and remove legacy eslint-plugin-dev
eslint.config.ts (prefer to upstream to the shared config if possible).@cypress/eslint-plugin-dev from the repo and CI.After all packages are migrated, simplify the lint-staged configuration in root package.json:
{
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
}
}
package.json/eslint.config.ts.npm/grep, npm/puppeteer, npm/mount-utils, npm/cypress-schematic.eslintrc* fileseslint.config.ts as aboveError: Error: You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.
Solution:
jiti: "^2.4.2" to the package's devDependenciesError: was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject
Solutions:
tsconfig.json that extends the base config:
{
"extends": "../../packages/ts/tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"allowJs": true,
"checkJs": false
},
"include": [
"src/**/*",
"cypress/**/*",
"*.js",
"*.ts",
"*.jsx",
"*.tsx"
],
"exclude": ["node_modules", "dist"]
}
cypress/tsconfig.json includes all test files:
{
"compilerOptions": {
"types": ["cypress"]
},
"include": [
"**/*.ts",
"**/*.js"
]
}
allowDefaultProject: true to ESLint config for problematic files:
{
files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx'],
languageOptions: {
parserOptions: {
allowDefaultProject: true,
},
},
}
Error: @cypress/dev/skip-comment rule violations for it.skip() tests
Solution:
eslint-disable-next-line @cypress/dev/skip-comment with proper explanatory comments:
// NOTE: This test is skipped for demonstration purposes
it.skip('first test', () => {})
Error: ESLint rule violations that don't match the package's ESLint configuration (e.g., Unexpected console statement when no-console is off in package config)
Root Cause: Pre-commit hook runs ESLint from root directory using root-level config, not package-specific config
Solution:
package.json now includes explicit lint-staged patterns for each package:
"lint-staged": {
"npm/grep/**/*.{js,jsx,ts,tsx}": "yarn lint:fix",
"*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
"cli/**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
"packages/**/*.{js,jsx,ts,tsx,json,eslintrc,vue}": "eslint --fix",
// ... explicit patterns for each directory
}
yarn lint:fix which runs the lerna command to execute ESLint from the package directoryeslint --fix which runs from the root with the root configBefore ESLint 9.x:
"lint": "eslint . --ext .js,.ts"
After ESLint 9.x:
"lint": "eslint"
ESLint 9.x auto-detects file extensions, so --ext flag is no longer needed.
Required additions to package.json:
{
"devDependencies": {
"@packages/eslint-config": "0.0.0-development",
"eslint": "^9.18.0",
"jiti": "^2.4.2"
}
}
Why jiti? ESLint 9 loads TypeScript flat config files (eslint.config.ts) at runtime via jiti, not via a static import in package source. Add it as an explicit devDependency in each migrated package so Yarn resolves jiti@^2.4.2 locally instead of a hoisted older version (see troubleshooting §1).
Why doesn't yarn health-check / knip flag jiti? Knip ties the eslint binary to eslint.config.ts as config infrastructure and does not treat jiti as an unused dependency in migrated npm packages. It is still required at lint runtime even though no source file imports it.
If a package needs custom rules, extend the base config:
import baseConfig from '@packages/eslint-config'
export default [
...baseConfig,
{
files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx'],
rules: {
'no-console': 'off',
'no-restricted-syntax': 'off',
},
},
{
files: ['cypress/**/*.js', 'cypress/**/*.ts'],
languageOptions: {
globals: {
Cypress: 'readonly',
cy: 'readonly',
},
},
},
]
For each package, ensure you've completed:
.eslintrc* fileseslint.config.ts with proper configurationeslint, @packages/eslint-config, jiti)package.jsontsconfig.json that extends base config--ext flag)yarn lint successfullyHappy linting!