From c10e50c58f71566af339150a3859785fa7ac1337 Mon Sep 17 00:00:00 2001 From: Mikael Korpela Date: Mon, 1 Jun 2026 14:40:29 +0300 Subject: [PATCH 1/3] Eslint: fix ignored files and remove file based exclusion mechanism --- .github/workflows/linting.yml | 58 ++----------------- package.json | 2 +- .../global-notices/state/notices/actions.js | 14 +++++ .../global-notices/state/notices/reducer.js | 7 +++ .../_inc/client/components/modal/index.jsx | 6 ++ .../_inc/client/components/popover/util.js | 44 ++++++++++++-- .../plugins/jetpack/_inc/client/config.js | 8 +++ .../_inc/client/lib/accessible-focus/index.js | 3 + .../_inc/client/mixins/emitter/index.js | 5 ++ .../jetpack/_inc/client/my-plan/index.jsx | 6 ++ .../plugins/jetpack/_inc/twitter-timeline.js | 2 +- .../jetpack/changelog/update-eslint-fixes | 4 ++ tools/cleanup-excludelists.sh | 7 +-- tools/eslint-excludelist.json | 11 ---- tools/js-tools/check-excludelist-diff.js | 14 ++--- tools/js-tools/git-hooks/pre-commit-hook.mjs | 58 +------------------ tools/js-tools/load-eslint-ignore.js | 11 ---- tools/list-repo-maintenance-commits.sh | 1 - 18 files changed, 107 insertions(+), 154 deletions(-) create mode 100644 projects/plugins/jetpack/changelog/update-eslint-fixes delete mode 100644 tools/eslint-excludelist.json diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index be34b010961f..27ba432f21fc 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -69,9 +69,6 @@ jobs: # JSON string holding an array of files in phpcs-excludelist.json that have changed. php_excluded_files: ${{ steps.filterPHP.outputs.php_excluded_files }} - # JSON string holding an array of files in eslint-excludelist.json that have changed. - js_excluded_files: ${{ steps.filterJS.outputs.js_excluded_files }} - # Whether any excluded files were modified or deleted. excludelist: ${{ steps.filterExcludeList.outputs.excluded_files != '[]' || steps.filter.outputs.misc == 'true' || steps.filter.outputs.misc_php == 'true' || steps.filter.outputs.misc_js == 'true' || steps.filter.outputs.misc_excludelist == 'true' }} @@ -175,8 +172,6 @@ jobs: - 'eslint.config.*' - '**/.eslintignore' - '**/eslint.config.*' - # If the excludelist changed, run to ensure newly non-excluded files pass. - - 'tools/eslint-excludelist.json' misc_css: # If package or stylelint config changed, there may be new checks. - 'package.json' @@ -219,24 +214,13 @@ jobs: echo "Excluded files:" jq --argjson files "$EXCLUDED_FILES" -nr '" - " + $files[]' - - if: github.event_name == 'pull_request' - id: filterJS - shell: bash - env: - JS_FILES: ${{ steps.filter.outputs.js_files }} - run: | - EXCLUDED_FILES=$(jq --argjson files "$JS_FILES" --slurpfile excludes tools/eslint-excludelist.json -nc '$files - ($files - $excludes[0])') - echo "js_excluded_files=$EXCLUDED_FILES" >> "$GITHUB_OUTPUT" - echo "Excluded files:" - jq --argjson files "$EXCLUDED_FILES" -nr '" - " + $files[]' - - if: github.event_name == 'pull_request' id: filterExcludeList shell: bash env: FILES: ${{ steps.filter.outputs.excludelist_files }} run: | - EXCLUDED_FILES=$(jq --argjson files "$FILES" --slurpfile phpexcludes tools/phpcs-excludelist.json --slurpfile jsexcludes tools/eslint-excludelist.json -nc '$files - ($files - $phpexcludes[0] - $jsexcludes[0])') + EXCLUDED_FILES=$(jq --argjson files "$FILES" --slurpfile phpexcludes tools/phpcs-excludelist.json -nc '$files - ($files - $phpexcludes[0])') echo "excluded_files=$EXCLUDED_FILES" >> "$GITHUB_OUTPUT" echo "Excluded files:" jq --argjson files "$EXCLUDED_FILES" -nr '" - " + $files[]' @@ -384,12 +368,12 @@ jobs: jq -r '.files | to_entries | .[] | .key as $key | .value.messages[] | [ $key, ":", .line, ":", .column, ": ", .type, " - ", .message, " (", .source, ")" ] | map(tostring) | join("")' echo "::remove-matcher owner=phpcs" - ### Runs eslint on JS files not listed in eslint-excludelist.json + ### Runs eslint on JS files not ignored by .eslintignore # Local equivalent: `pnpm run lint-required` - # On trunk: runs on all non-excluded JS files + # On trunk: runs on all non-ignored JS files # On PRs: runs only if JS files or relevant config changed eslint: - name: ESLint (non-excluded files only) + name: ESLint runs-on: ubuntu-latest needs: changed_files if: github.event_name == 'push' || needs.changed_files.outputs.js == 'true' || needs.changed_files.outputs.misc_js == 'true' @@ -407,40 +391,6 @@ jobs: run: pnpm install - run: pnpm run lint-required - ### Runs eslint-changed on JS files listed in eslint-excludelist.json. - # Local equivalent: `pnpm run lint-changed --git-base=` - # `` is the branch this PR is to be merged into, probably `origin/trunk`. - # - # Pre-commit, you might also `git add` the relevant files and run `pnpm run lint-changed` - eslint_changed: - name: ESLint (changes to excluded files only) - runs-on: ubuntu-latest - needs: changed_files - if: github.event_name == 'pull_request' && needs.changed_files.outputs.js_excluded_files != '[]' - continue-on-error: true - timeout-minutes: 10 # 2025-11-06: Takes about a minute, but rarely runs. - - steps: - # We don't need full git history, but eslint-changed does need everything up to the merge-base. - - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 10 - - uses: ./.github/actions/deepen-to-merge-base - - - name: Setup tools - uses: ./.github/actions/tool-setup - with: - php: false - - - name: Monorepo pnpm install - run: pnpm install - - name: Run eslint-changed - env: - SHA: ${{ github.event.pull_request.base.sha }} - FILES: ${{ needs.changed_files.outputs.js_excluded_files }} - run: pnpm run lint-changed --git-base=$SHA $(jq -rn --argjson files "$FILES" '$files[]') - ### Runs lint-style on all CSS/SCSS files in the monorepo except those ignored in .stylelintignore. # Local equivalent: `pnpm run lint-style .` # On trunk: runs on all CSS/SCSS files diff --git a/package.json b/package.json index 4a4435f4d523..dc59c59403f0 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint": "pnpm run lint-file .", "lint-changed": "eslint-changed --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.svelte --eslint-options flags='[\"v10_config_lookup_from_file\"]' --git", "lint-file": "eslint --flag v10_config_lookup_from_file", - "lint-required": "ESLINT_IGNORE_REQUIRED=1 pnpm run lint --max-warnings=0", + "lint-required": "pnpm run lint --max-warnings=0", "lint-style": "stylelint --allow-empty-input --globby-options '{\"gitignore\":true,\"ignore\":[\"tools/docker/data\",\"**/vendor/**\",\"**/jetpack_vendor/**\"]}'", "php:autofix": "composer phpcs:fix", "php:compatibility": "composer phpcs:compatibility", diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/actions.js b/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/actions.js index ac29f39ac2a9..fcc6ae1e679f 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/actions.js +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/actions.js @@ -1,6 +1,12 @@ import { uniqueId } from 'lodash'; import { NEW_NOTICE, REMOVE_NOTICE } from '../action-types'; +/** + * Creates a remove notice action. + * + * @param {string} noticeId - Notice ID. + * @return {object} Action object. + */ export function removeNotice( noticeId ) { return { noticeId: noticeId, @@ -8,6 +14,14 @@ export function removeNotice( noticeId ) { }; } +/** + * Creates a new notice action. + * + * @param {string} status - Notice status. + * @param {string} text - Notice text. + * @param {object} options - Notice options. + * @return {object} Action object. + */ export function createNotice( status, text, options = {} ) { const notice = { noticeId: options.id || uniqueId(), diff --git a/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/reducer.js b/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/reducer.js index 3f7d3b491d81..0d9ee8531559 100644 --- a/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/reducer.js +++ b/projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/reducer.js @@ -1,6 +1,13 @@ import { combineReducers } from 'redux'; import { NEW_NOTICE, REMOVE_NOTICE } from '../action-types'; +/** + * Reducer for global notices. + * + * @param {Array} state - Current state. + * @param {object} action - Action object. + * @return {Array} Updated state. + */ export function globalNotices( state = [], action ) { switch ( action.type ) { case NEW_NOTICE: diff --git a/projects/plugins/jetpack/_inc/client/components/modal/index.jsx b/projects/plugins/jetpack/_inc/client/components/modal/index.jsx index 7bd0f977893e..45dd4122c0ee 100644 --- a/projects/plugins/jetpack/_inc/client/components/modal/index.jsx +++ b/projects/plugins/jetpack/_inc/client/components/modal/index.jsx @@ -17,10 +17,16 @@ let preventCloseFlag = false; import './style.scss'; +/** + * Prevents any modals from closing until {@link allowClose} is called. + */ function preventClose() { preventCloseFlag = true; } +/** + * Allows modals to close again after {@link preventClose} was called. + */ function allowClose() { preventCloseFlag = false; } diff --git a/projects/plugins/jetpack/_inc/client/components/popover/util.js b/projects/plugins/jetpack/_inc/client/components/popover/util.js index 6e5ff3e76ba4..b88f928b18bb 100644 --- a/projects/plugins/jetpack/_inc/client/components/popover/util.js +++ b/projects/plugins/jetpack/_inc/client/components/popover/util.js @@ -7,6 +7,12 @@ import debugFactory from 'debug'; const debug = debugFactory( 'calypso:popover:util' ); // inspired by https://github.com/jkroso/viewport + +/** + * Updates and returns the current viewport dimensions. + * + * @return {object} Viewport object with top, left, width, height, right, and bottom. + */ function updateViewport() { const viewport = {}; viewport.top = window.scrollY; @@ -38,6 +44,9 @@ const adjacent = { let viewport = updateViewport(); +/** + * Updates the cached viewport on window resize or scroll. + */ function onViewportChange() { viewport = updateViewport(); } @@ -95,6 +104,13 @@ const suggested = ( pos, el, target ) => { return chooseSecondary( primary, pos1, el, target, w, h ) || pos; }; +/** + * Chooses the primary popover position based on available room. + * + * @param {string} prefered - Preferred position. + * @param {object} room - Available room in each direction. + * @return {string|undefined} Best primary position. + */ function choosePrimary( prefered, room ) { // top, bottom, left, right in order of preference const order = [ @@ -125,6 +141,17 @@ function choosePrimary( prefered, room ) { return bestPos; } +/** + * Chooses the secondary popover position based on visible area. + * + * @param {string} primary - Primary position. + * @param {string|null} prefered - Preferred secondary position. + * @param {HTMLElement} el - Tip element. + * @param {HTMLElement} target - Target element. + * @param {number} w - Tip width. + * @param {number} h - Tip height. + * @return {string|undefined} Best position string. + */ function chooseSecondary( primary, prefered, el, target, w, h ) { // top, top left, top right in order of preference const order = prefered @@ -170,6 +197,14 @@ function chooseSecondary( primary, prefered, el, target, w, h ) { return bestPos; } +/** + * Calculates the offset for a popover position. + * + * @param {string} pos - Position string. + * @param {HTMLElement} el - Tip element. + * @param {HTMLElement} target - Target element. + * @return {object} Offset with top and left properties. + */ function offset( pos, el, target ) { const pad = 15; const tipRect = getBoundingClientRect( el ); @@ -290,14 +325,13 @@ function offset( pos, el, target ) { /** * Extracted from `timoxley/offset`, but directly using a - * TextRectangle instead of getting another version. + * DOMRect instead of getting another version. * - * @param {TextRectangle} box - result from a `getBoundingClientRect()` call - * @param {Document} doc - Document instance to use - * @return {object} an object with `top` and `left` Number properties + * @param {DOMRect} box - Result from a `getBoundingClientRect()` call. + * @param {Document} doc - Document instance to use. + * @return {object} An object with `top` and `left` number properties. * @private */ - function _offset( box, doc ) { const body = doc.body || doc.getElementsByTagName( 'body' )[ 0 ]; const docEl = doc.documentElement || body.parentNode; diff --git a/projects/plugins/jetpack/_inc/client/config.js b/projects/plugins/jetpack/_inc/client/config.js index 3de626c01622..c7c5d7d348ff 100644 --- a/projects/plugins/jetpack/_inc/client/config.js +++ b/projects/plugins/jetpack/_inc/client/config.js @@ -4,6 +4,14 @@ const data = { google_analytics_enabled: false, google_analytics_key: null, }; + +/** + * Returns a configuration value. + * + * @param {string} key - Configuration key. + * @return {*} Configuration value. + * @throws {Error} If the key does not exist. + */ function config( key ) { if ( key in data ) { return data[ key ]; diff --git a/projects/plugins/jetpack/_inc/client/lib/accessible-focus/index.js b/projects/plugins/jetpack/_inc/client/lib/accessible-focus/index.js index 383c47cef608..cc451c876359 100644 --- a/projects/plugins/jetpack/_inc/client/lib/accessible-focus/index.js +++ b/projects/plugins/jetpack/_inc/client/lib/accessible-focus/index.js @@ -1,6 +1,9 @@ const keyboardNavigationKeycodes = [ 9, 32, 37, 38, 39, 40 ]; // keyCodes for tab, space, left, up, right, down respectively let keyboardNavigation = false; +/** + * Enables accessible focus styles when navigating via keyboard. + */ function accessibleFocus() { document.addEventListener( 'keydown', function ( event ) { if ( keyboardNavigation ) { diff --git a/projects/plugins/jetpack/_inc/client/mixins/emitter/index.js b/projects/plugins/jetpack/_inc/client/mixins/emitter/index.js index c1ec66c0de04..57cfd6a808ec 100644 --- a/projects/plugins/jetpack/_inc/client/mixins/emitter/index.js +++ b/projects/plugins/jetpack/_inc/client/mixins/emitter/index.js @@ -1,5 +1,10 @@ import { EventEmitter } from 'events'; +/** + * Adds EventEmitter methods to a prototype. + * + * @param {object} prototype - Prototype to extend. + */ export default function ( prototype ) { Object.assign( prototype, EventEmitter.prototype ); prototype.emitChange = function () { diff --git a/projects/plugins/jetpack/_inc/client/my-plan/index.jsx b/projects/plugins/jetpack/_inc/client/my-plan/index.jsx index dbbfd92d05f0..fdbb163c3208 100644 --- a/projects/plugins/jetpack/_inc/client/my-plan/index.jsx +++ b/projects/plugins/jetpack/_inc/client/my-plan/index.jsx @@ -15,6 +15,12 @@ import MyPlanBody from './my-plan-body'; import MyPlanHeader from './my-plan-header'; import MyPlanPartnerCoupon from './my-plan-partner-coupon'; +/** + * My Plan page component. + * + * @param {object} props - Component props. + * @return {import('react').ReactElement} React element. + */ export function MyPlan( props ) { let sitePlan = props.sitePlan.product_slug || '', availableFeatures = props.availableFeatures, diff --git a/projects/plugins/jetpack/_inc/twitter-timeline.js b/projects/plugins/jetpack/_inc/twitter-timeline.js index ff3dc79f162a..d747b1265336 100644 --- a/projects/plugins/jetpack/_inc/twitter-timeline.js +++ b/projects/plugins/jetpack/_inc/twitter-timeline.js @@ -1,4 +1,4 @@ -! ( function ( d, s, id ) { +void ( function ( d, s, id ) { var js, fjs = d.getElementsByTagName( s )[ 0 ], p = /^http:/.test( d.location ) ? 'http' : 'https'; diff --git a/projects/plugins/jetpack/changelog/update-eslint-fixes b/projects/plugins/jetpack/changelog/update-eslint-fixes new file mode 100644 index 000000000000..fb282639d14f --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-eslint-fixes @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Internal: Fix ESLint issues in legacy Jetpack admin client files. diff --git a/tools/cleanup-excludelists.sh b/tools/cleanup-excludelists.sh index 5f49fc7e3b2f..7e057f0cbcd0 100755 --- a/tools/cleanup-excludelists.sh +++ b/tools/cleanup-excludelists.sh @@ -12,14 +12,9 @@ if [[ ! -e "$TEMP" ]]; then fi trap 'rm "$TEMP"' EXIT -: > "$TEMP" -pnpm run lint-file --max-warnings=0 --format=json --output-file="$TEMP" $(for f in $(jq -r '.[]' tools/eslint-excludelist.json); do [[ -e "$f" ]] && echo $f; done) || true -[[ -s "$TEMP" ]] && jq -e '.' < "$TEMP" >/dev/null || die "No JSON data found" -jq --tab -r --arg pwd "$PWD/" '[ .[] | select( .messages[0]?.ruleId ) | .filePath | ltrimstr($pwd) ] | sort' "$TEMP" > tools/eslint-excludelist.json - : > "$TEMP" composer phpcs:lint -- -m --file-list=<(for f in $(jq -r '.[]' tools/phpcs-excludelist.json); do [[ -e "$f" ]] && echo $f; done) --report=json --report-file="$TEMP" || true [[ -s "$TEMP" ]] && jq -e '.' < "$TEMP" >/dev/null || die "No JSON data found" jq --tab -r --arg pwd "$PWD/" '[ .files | to_entries | .[] | select( .value.errors > 0 or .value.warnings > 0 ) | .key | ltrimstr($pwd) ] | sort' "$TEMP" > tools/phpcs-excludelist.json -git diff tools/eslint-excludelist.json tools/phpcs-excludelist.json +git diff tools/phpcs-excludelist.json diff --git a/tools/eslint-excludelist.json b/tools/eslint-excludelist.json deleted file mode 100644 index 9671bc7c5724..000000000000 --- a/tools/eslint-excludelist.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - "projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/actions.js", - "projects/plugins/jetpack/_inc/client/components/global-notices/state/notices/reducer.js", - "projects/plugins/jetpack/_inc/client/components/modal/index.jsx", - "projects/plugins/jetpack/_inc/client/components/popover/util.js", - "projects/plugins/jetpack/_inc/client/config.js", - "projects/plugins/jetpack/_inc/client/lib/accessible-focus/index.js", - "projects/plugins/jetpack/_inc/client/mixins/emitter/index.js", - "projects/plugins/jetpack/_inc/client/my-plan/index.jsx", - "projects/plugins/jetpack/_inc/twitter-timeline.js" -] diff --git a/tools/js-tools/check-excludelist-diff.js b/tools/js-tools/check-excludelist-diff.js index ce2fcc8ff3bb..d6b3bfb67f9e 100755 --- a/tools/js-tools/check-excludelist-diff.js +++ b/tools/js-tools/check-excludelist-diff.js @@ -3,15 +3,11 @@ const spawnSync = require( 'child_process' ).spawnSync; const parseDiff = require( 'parse-diff' ); -const res = spawnSync( - 'git', - [ 'diff', 'tools/eslint-excludelist.json', 'tools/phpcs-excludelist.json' ], - { - stdio: [ 'ignore', 'pipe', 'inherit' ], - maxBuffer: Infinity, - encoding: 'utf8', - } -); +const res = spawnSync( 'git', [ 'diff', 'tools/phpcs-excludelist.json' ], { + stdio: [ 'ignore', 'pipe', 'inherit' ], + maxBuffer: Infinity, + encoding: 'utf8', +} ); if ( res.status ) { process.exit( res.status ); } diff --git a/tools/js-tools/git-hooks/pre-commit-hook.mjs b/tools/js-tools/git-hooks/pre-commit-hook.mjs index e57c9d04b287..a975c3f8b1c1 100644 --- a/tools/js-tools/git-hooks/pre-commit-hook.mjs +++ b/tools/js-tools/git-hooks/pre-commit-hook.mjs @@ -11,7 +11,6 @@ import loadIgnorePatterns from '../load-eslint-ignore.js'; import isJetpackDraftMode from './jetpack-draft.mjs'; let phpcsExcludelist = null; -let eslintExcludelist = null; let eslintIgnore = null; let exitCode = 0; @@ -31,20 +30,6 @@ function loadPhpcsExcludeList() { return phpcsExcludelist; } -/** - * Load the eslint exclude list. - * - * @return {Array} Files to exclude. - */ -function loadEslintExcludeList() { - if ( null === eslintExcludelist ) { - eslintExcludelist = JSON.parse( - fs.readFileSync( __dirname + '/../../eslint-excludelist.json', 'utf8' ) - ); - } - return eslintExcludelist; -} - /** * Apply .eslintignore to a list of files. * @@ -106,16 +91,6 @@ function filterJsFiles( file ) { ); } -/** - * Filter callback for JS files - * - * @param {string} file - dirty file - * @return {boolean} whether file needs to be linted - */ -function filterEslintFiles( file ) { - return -1 === loadEslintExcludeList().findIndex( filePath => file === filePath ); -} - /** * Provides filter to determine which CSS/SCSS files to run through linting. * @@ -295,26 +270,6 @@ function runEslintFix( toFixFiles ) { } } -/** - * Run eslint-changed - * - * @param {Array} toLintFiles - List of files to lint - */ -function runEslintChanged( toLintFiles ) { - toLintFiles = applyEslintIgnore( toLintFiles ); - if ( ! toLintFiles.length ) { - return; - } - - const eslintResult = spawnSync( 'pnpm', [ 'run', 'lint-changed', ...toLintFiles ], { - stdio: 'inherit', - } ); - - if ( eslintResult && eslintResult.status && ! isJetpackDraftMode() ) { - checkFailed(); - } -} - /** * Run php:lint * @@ -501,12 +456,8 @@ dirtyFiles.forEach( file => // Start JS work—linting, prettify, etc. -const eslintFiles = jsFiles.filter( filterEslintFiles ); -const eslintFixFiles = eslintFiles.filter( file => checkFileAgainstDirtyList( file, dirtyFiles ) ); -const eslintNoFixFiles = eslintFiles.filter( - file => ! checkFileAgainstDirtyList( file, dirtyFiles ) -); -const eslintChangedFiles = jsFiles.filter( file => ! filterEslintFiles( file ) ); +const eslintFixFiles = jsFiles.filter( file => checkFileAgainstDirtyList( file, dirtyFiles ) ); +const eslintNoFixFiles = jsFiles.filter( file => ! checkFileAgainstDirtyList( file, dirtyFiles ) ); const toPrettify = jsFiles.filter( file => checkFileAgainstDirtyList( file, dirtyFiles ) ); toPrettify.forEach( file => console.log( `Prettier formatting staged file: ${ file }` ) ); @@ -517,13 +468,10 @@ if ( toPrettify.length ) { } // linting should happen after formatting -if ( eslintFiles.length > 0 ) { +if ( jsFiles.length > 0 ) { runEslintFix( eslintFixFiles ); runEslint( eslintNoFixFiles ); } -if ( eslintChangedFiles.length > 0 ) { - runEslintChanged( eslintChangedFiles ); -} // Start PHP work. diff --git a/tools/js-tools/load-eslint-ignore.js b/tools/js-tools/load-eslint-ignore.js index e10465d78109..bd719b5f170c 100644 --- a/tools/js-tools/load-eslint-ignore.js +++ b/tools/js-tools/load-eslint-ignore.js @@ -140,17 +140,6 @@ function loadIgnorePatterns( basedir ) { } } - if ( process.env.ESLINT_IGNORE_REQUIRED ) { - for ( const file of JSON.parse( - fs.readFileSync( path.join( __dirname, '../eslint-excludelist.json' ) ) - ) ) { - const f = path.relative( basedir, path.resolve( rootdir, file ) ); - if ( ! f.startsWith( '../' ) ) { - rules.push( f ); - } - } - } - if ( debugrules.enabled ) { debugrules( // prettier-ignore diff --git a/tools/list-repo-maintenance-commits.sh b/tools/list-repo-maintenance-commits.sh index a31efb77d09b..6b225ca66676 100755 --- a/tools/list-repo-maintenance-commits.sh +++ b/tools/list-repo-maintenance-commits.sh @@ -18,7 +18,6 @@ PATHS=( ':!.phan/stubs/*' :!pnpm-lock.yaml :!tools/phpcs-excludelist.json - :!tools/eslint-excludelist.json ':!tools/stubs/*-defs.php' ':!*/composer.lock' ':!*/changelog/*' From 02021dbd3ef57dd496a26bd8d1b63790fb971529 Mon Sep 17 00:00:00 2001 From: Mikael Korpela Date: Mon, 1 Jun 2026 15:57:11 +0300 Subject: [PATCH 2/3] Update projects/plugins/jetpack/changelog/update-eslint-fixes Co-authored-by: Brad Jorsch --- projects/plugins/jetpack/changelog/update-eslint-fixes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/jetpack/changelog/update-eslint-fixes b/projects/plugins/jetpack/changelog/update-eslint-fixes index fb282639d14f..f8a0470dff03 100644 --- a/projects/plugins/jetpack/changelog/update-eslint-fixes +++ b/projects/plugins/jetpack/changelog/update-eslint-fixes @@ -1,4 +1,4 @@ Significance: patch Type: other +Comment: Fix ESLint issues in legacy Jetpack admin client files. -Internal: Fix ESLint issues in legacy Jetpack admin client files. From 00d67cdb159d3f17cc5b8843417285e6605ac861 Mon Sep 17 00:00:00 2001 From: Mikael Korpela Date: Mon, 1 Jun 2026 19:20:11 +0300 Subject: [PATCH 3/3] Revert tooling changes, keep lints sorted --- .github/workflows/linting.yml | 58 ++++++++++++++++++-- package.json | 2 +- tools/cleanup-excludelists.sh | 7 ++- tools/eslint-excludelist.json | 1 + tools/js-tools/check-excludelist-diff.js | 14 +++-- tools/js-tools/git-hooks/pre-commit-hook.mjs | 58 +++++++++++++++++++- tools/js-tools/load-eslint-ignore.js | 11 ++++ tools/list-repo-maintenance-commits.sh | 1 + 8 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 tools/eslint-excludelist.json diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 27ba432f21fc..be34b010961f 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -69,6 +69,9 @@ jobs: # JSON string holding an array of files in phpcs-excludelist.json that have changed. php_excluded_files: ${{ steps.filterPHP.outputs.php_excluded_files }} + # JSON string holding an array of files in eslint-excludelist.json that have changed. + js_excluded_files: ${{ steps.filterJS.outputs.js_excluded_files }} + # Whether any excluded files were modified or deleted. excludelist: ${{ steps.filterExcludeList.outputs.excluded_files != '[]' || steps.filter.outputs.misc == 'true' || steps.filter.outputs.misc_php == 'true' || steps.filter.outputs.misc_js == 'true' || steps.filter.outputs.misc_excludelist == 'true' }} @@ -172,6 +175,8 @@ jobs: - 'eslint.config.*' - '**/.eslintignore' - '**/eslint.config.*' + # If the excludelist changed, run to ensure newly non-excluded files pass. + - 'tools/eslint-excludelist.json' misc_css: # If package or stylelint config changed, there may be new checks. - 'package.json' @@ -214,13 +219,24 @@ jobs: echo "Excluded files:" jq --argjson files "$EXCLUDED_FILES" -nr '" - " + $files[]' + - if: github.event_name == 'pull_request' + id: filterJS + shell: bash + env: + JS_FILES: ${{ steps.filter.outputs.js_files }} + run: | + EXCLUDED_FILES=$(jq --argjson files "$JS_FILES" --slurpfile excludes tools/eslint-excludelist.json -nc '$files - ($files - $excludes[0])') + echo "js_excluded_files=$EXCLUDED_FILES" >> "$GITHUB_OUTPUT" + echo "Excluded files:" + jq --argjson files "$EXCLUDED_FILES" -nr '" - " + $files[]' + - if: github.event_name == 'pull_request' id: filterExcludeList shell: bash env: FILES: ${{ steps.filter.outputs.excludelist_files }} run: | - EXCLUDED_FILES=$(jq --argjson files "$FILES" --slurpfile phpexcludes tools/phpcs-excludelist.json -nc '$files - ($files - $phpexcludes[0])') + EXCLUDED_FILES=$(jq --argjson files "$FILES" --slurpfile phpexcludes tools/phpcs-excludelist.json --slurpfile jsexcludes tools/eslint-excludelist.json -nc '$files - ($files - $phpexcludes[0] - $jsexcludes[0])') echo "excluded_files=$EXCLUDED_FILES" >> "$GITHUB_OUTPUT" echo "Excluded files:" jq --argjson files "$EXCLUDED_FILES" -nr '" - " + $files[]' @@ -368,12 +384,12 @@ jobs: jq -r '.files | to_entries | .[] | .key as $key | .value.messages[] | [ $key, ":", .line, ":", .column, ": ", .type, " - ", .message, " (", .source, ")" ] | map(tostring) | join("")' echo "::remove-matcher owner=phpcs" - ### Runs eslint on JS files not ignored by .eslintignore + ### Runs eslint on JS files not listed in eslint-excludelist.json # Local equivalent: `pnpm run lint-required` - # On trunk: runs on all non-ignored JS files + # On trunk: runs on all non-excluded JS files # On PRs: runs only if JS files or relevant config changed eslint: - name: ESLint + name: ESLint (non-excluded files only) runs-on: ubuntu-latest needs: changed_files if: github.event_name == 'push' || needs.changed_files.outputs.js == 'true' || needs.changed_files.outputs.misc_js == 'true' @@ -391,6 +407,40 @@ jobs: run: pnpm install - run: pnpm run lint-required + ### Runs eslint-changed on JS files listed in eslint-excludelist.json. + # Local equivalent: `pnpm run lint-changed --git-base=` + # `` is the branch this PR is to be merged into, probably `origin/trunk`. + # + # Pre-commit, you might also `git add` the relevant files and run `pnpm run lint-changed` + eslint_changed: + name: ESLint (changes to excluded files only) + runs-on: ubuntu-latest + needs: changed_files + if: github.event_name == 'pull_request' && needs.changed_files.outputs.js_excluded_files != '[]' + continue-on-error: true + timeout-minutes: 10 # 2025-11-06: Takes about a minute, but rarely runs. + + steps: + # We don't need full git history, but eslint-changed does need everything up to the merge-base. + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 10 + - uses: ./.github/actions/deepen-to-merge-base + + - name: Setup tools + uses: ./.github/actions/tool-setup + with: + php: false + + - name: Monorepo pnpm install + run: pnpm install + - name: Run eslint-changed + env: + SHA: ${{ github.event.pull_request.base.sha }} + FILES: ${{ needs.changed_files.outputs.js_excluded_files }} + run: pnpm run lint-changed --git-base=$SHA $(jq -rn --argjson files "$FILES" '$files[]') + ### Runs lint-style on all CSS/SCSS files in the monorepo except those ignored in .stylelintignore. # Local equivalent: `pnpm run lint-style .` # On trunk: runs on all CSS/SCSS files diff --git a/package.json b/package.json index dc59c59403f0..4a4435f4d523 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint": "pnpm run lint-file .", "lint-changed": "eslint-changed --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.svelte --eslint-options flags='[\"v10_config_lookup_from_file\"]' --git", "lint-file": "eslint --flag v10_config_lookup_from_file", - "lint-required": "pnpm run lint --max-warnings=0", + "lint-required": "ESLINT_IGNORE_REQUIRED=1 pnpm run lint --max-warnings=0", "lint-style": "stylelint --allow-empty-input --globby-options '{\"gitignore\":true,\"ignore\":[\"tools/docker/data\",\"**/vendor/**\",\"**/jetpack_vendor/**\"]}'", "php:autofix": "composer phpcs:fix", "php:compatibility": "composer phpcs:compatibility", diff --git a/tools/cleanup-excludelists.sh b/tools/cleanup-excludelists.sh index 7e057f0cbcd0..5f49fc7e3b2f 100755 --- a/tools/cleanup-excludelists.sh +++ b/tools/cleanup-excludelists.sh @@ -12,9 +12,14 @@ if [[ ! -e "$TEMP" ]]; then fi trap 'rm "$TEMP"' EXIT +: > "$TEMP" +pnpm run lint-file --max-warnings=0 --format=json --output-file="$TEMP" $(for f in $(jq -r '.[]' tools/eslint-excludelist.json); do [[ -e "$f" ]] && echo $f; done) || true +[[ -s "$TEMP" ]] && jq -e '.' < "$TEMP" >/dev/null || die "No JSON data found" +jq --tab -r --arg pwd "$PWD/" '[ .[] | select( .messages[0]?.ruleId ) | .filePath | ltrimstr($pwd) ] | sort' "$TEMP" > tools/eslint-excludelist.json + : > "$TEMP" composer phpcs:lint -- -m --file-list=<(for f in $(jq -r '.[]' tools/phpcs-excludelist.json); do [[ -e "$f" ]] && echo $f; done) --report=json --report-file="$TEMP" || true [[ -s "$TEMP" ]] && jq -e '.' < "$TEMP" >/dev/null || die "No JSON data found" jq --tab -r --arg pwd "$PWD/" '[ .files | to_entries | .[] | select( .value.errors > 0 or .value.warnings > 0 ) | .key | ltrimstr($pwd) ] | sort' "$TEMP" > tools/phpcs-excludelist.json -git diff tools/phpcs-excludelist.json +git diff tools/eslint-excludelist.json tools/phpcs-excludelist.json diff --git a/tools/eslint-excludelist.json b/tools/eslint-excludelist.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/tools/eslint-excludelist.json @@ -0,0 +1 @@ +[] diff --git a/tools/js-tools/check-excludelist-diff.js b/tools/js-tools/check-excludelist-diff.js index d6b3bfb67f9e..ce2fcc8ff3bb 100755 --- a/tools/js-tools/check-excludelist-diff.js +++ b/tools/js-tools/check-excludelist-diff.js @@ -3,11 +3,15 @@ const spawnSync = require( 'child_process' ).spawnSync; const parseDiff = require( 'parse-diff' ); -const res = spawnSync( 'git', [ 'diff', 'tools/phpcs-excludelist.json' ], { - stdio: [ 'ignore', 'pipe', 'inherit' ], - maxBuffer: Infinity, - encoding: 'utf8', -} ); +const res = spawnSync( + 'git', + [ 'diff', 'tools/eslint-excludelist.json', 'tools/phpcs-excludelist.json' ], + { + stdio: [ 'ignore', 'pipe', 'inherit' ], + maxBuffer: Infinity, + encoding: 'utf8', + } +); if ( res.status ) { process.exit( res.status ); } diff --git a/tools/js-tools/git-hooks/pre-commit-hook.mjs b/tools/js-tools/git-hooks/pre-commit-hook.mjs index a975c3f8b1c1..e57c9d04b287 100644 --- a/tools/js-tools/git-hooks/pre-commit-hook.mjs +++ b/tools/js-tools/git-hooks/pre-commit-hook.mjs @@ -11,6 +11,7 @@ import loadIgnorePatterns from '../load-eslint-ignore.js'; import isJetpackDraftMode from './jetpack-draft.mjs'; let phpcsExcludelist = null; +let eslintExcludelist = null; let eslintIgnore = null; let exitCode = 0; @@ -30,6 +31,20 @@ function loadPhpcsExcludeList() { return phpcsExcludelist; } +/** + * Load the eslint exclude list. + * + * @return {Array} Files to exclude. + */ +function loadEslintExcludeList() { + if ( null === eslintExcludelist ) { + eslintExcludelist = JSON.parse( + fs.readFileSync( __dirname + '/../../eslint-excludelist.json', 'utf8' ) + ); + } + return eslintExcludelist; +} + /** * Apply .eslintignore to a list of files. * @@ -91,6 +106,16 @@ function filterJsFiles( file ) { ); } +/** + * Filter callback for JS files + * + * @param {string} file - dirty file + * @return {boolean} whether file needs to be linted + */ +function filterEslintFiles( file ) { + return -1 === loadEslintExcludeList().findIndex( filePath => file === filePath ); +} + /** * Provides filter to determine which CSS/SCSS files to run through linting. * @@ -270,6 +295,26 @@ function runEslintFix( toFixFiles ) { } } +/** + * Run eslint-changed + * + * @param {Array} toLintFiles - List of files to lint + */ +function runEslintChanged( toLintFiles ) { + toLintFiles = applyEslintIgnore( toLintFiles ); + if ( ! toLintFiles.length ) { + return; + } + + const eslintResult = spawnSync( 'pnpm', [ 'run', 'lint-changed', ...toLintFiles ], { + stdio: 'inherit', + } ); + + if ( eslintResult && eslintResult.status && ! isJetpackDraftMode() ) { + checkFailed(); + } +} + /** * Run php:lint * @@ -456,8 +501,12 @@ dirtyFiles.forEach( file => // Start JS work—linting, prettify, etc. -const eslintFixFiles = jsFiles.filter( file => checkFileAgainstDirtyList( file, dirtyFiles ) ); -const eslintNoFixFiles = jsFiles.filter( file => ! checkFileAgainstDirtyList( file, dirtyFiles ) ); +const eslintFiles = jsFiles.filter( filterEslintFiles ); +const eslintFixFiles = eslintFiles.filter( file => checkFileAgainstDirtyList( file, dirtyFiles ) ); +const eslintNoFixFiles = eslintFiles.filter( + file => ! checkFileAgainstDirtyList( file, dirtyFiles ) +); +const eslintChangedFiles = jsFiles.filter( file => ! filterEslintFiles( file ) ); const toPrettify = jsFiles.filter( file => checkFileAgainstDirtyList( file, dirtyFiles ) ); toPrettify.forEach( file => console.log( `Prettier formatting staged file: ${ file }` ) ); @@ -468,10 +517,13 @@ if ( toPrettify.length ) { } // linting should happen after formatting -if ( jsFiles.length > 0 ) { +if ( eslintFiles.length > 0 ) { runEslintFix( eslintFixFiles ); runEslint( eslintNoFixFiles ); } +if ( eslintChangedFiles.length > 0 ) { + runEslintChanged( eslintChangedFiles ); +} // Start PHP work. diff --git a/tools/js-tools/load-eslint-ignore.js b/tools/js-tools/load-eslint-ignore.js index bd719b5f170c..e10465d78109 100644 --- a/tools/js-tools/load-eslint-ignore.js +++ b/tools/js-tools/load-eslint-ignore.js @@ -140,6 +140,17 @@ function loadIgnorePatterns( basedir ) { } } + if ( process.env.ESLINT_IGNORE_REQUIRED ) { + for ( const file of JSON.parse( + fs.readFileSync( path.join( __dirname, '../eslint-excludelist.json' ) ) + ) ) { + const f = path.relative( basedir, path.resolve( rootdir, file ) ); + if ( ! f.startsWith( '../' ) ) { + rules.push( f ); + } + } + } + if ( debugrules.enabled ) { debugrules( // prettier-ignore diff --git a/tools/list-repo-maintenance-commits.sh b/tools/list-repo-maintenance-commits.sh index 6b225ca66676..a31efb77d09b 100755 --- a/tools/list-repo-maintenance-commits.sh +++ b/tools/list-repo-maintenance-commits.sh @@ -18,6 +18,7 @@ PATHS=( ':!.phan/stubs/*' :!pnpm-lock.yaml :!tools/phpcs-excludelist.json + :!tools/eslint-excludelist.json ':!tools/stubs/*-defs.php' ':!*/composer.lock' ':!*/changelog/*'