diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index f0fe323187..0626f07353 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -1,15 +1,21 @@ --- -name: "LuCI repo ESLint JSON Analysis" +name: "LuCI repo ESLint JS/ON and MD Analysis" on: push: branches: [ "master" ] path: - '**/*.json' + - '**/*.js' + - '**/*.md' + pull_request: branches: [ "master" ] path: - '**/*.json' + - '**/*.js' + - '**/*.md' + permissions: {} jobs: @@ -17,17 +23,57 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 + with: + fetch-depth: 2 - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: latest + # eslint/json requires eslint@9 - 10 is coming. - name: Install ESLint - run: npm install --no-audit --no-fund --save-dev eslint@latest @eslint/json@latest + run: | + npm install --no-audit --no-fund --save-dev \ + eslint@9 \ + @eslint/json@latest \ + @eslint/js \ + @eslint/markdown \ + eslint-formatter-gha - # Currently, we lint JSON only. - - name: Run ESLint - run: npx eslint **/*.json + # - name: Run ESLint (on whole repo) + # run: npx eslint . + + - name: Run ESLint on changed files + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + # push event: always diff last commit + BASE="$(git rev-parse HEAD~1 2>/dev/null || true)" + HEAD="$(git rev-parse HEAD)" + fi + + if [ -z "$BASE" ]; then + FILES=$(git ls-files '*.js' '*.json' '*.md') + else + FILES=$(git diff --diff-filter=ACM --name-only "$BASE" "$HEAD" \ + | grep -E '\.(js|json|md)$' || true) + fi + + if [ -z "$FILES" ]; then + echo "No JS/JSON or MD files changed" + exit 0 + fi + + echo "Linting files:" + echo "$FILES" + + # One day we might need xargs with huge lists + # echo "$FILES" | xargs npx eslint -f gha + + # Until then, do it simply so we can see which error relates to which file + npx eslint $FILES diff --git a/eslint.config.mjs b/eslint.config.mjs index 87f38a6e44..36ba788108 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,13 +1,189 @@ -import { defineConfig } from "eslint/config"; -import json from "@eslint/json"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import globals from 'globals'; +import markdown from "@eslint/markdown"; +import jsdoc from 'eslint-plugin-jsdoc'; +import json from '@eslint/json'; +import js from '@eslint/js'; + +console.log("loaded luci repo eslint.config.mjs"); + +export const jsdoc_less_relaxed_rules = { + // 0: off, 1: warn, 2: error + /* --- JSDoc correctness --- */ + 'jsdoc/check-alignment': 'warn', + 'jsdoc/check-param-names': 'off', + 'jsdoc/check-tag-names': 'warn', + 'jsdoc/check-types': 'off', + 'jsdoc/no-defaults': 'off', + 'jsdoc/reject-any-type': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'warn', + 'jsdoc/require-returns': 'warn', + 'jsdoc/require-returns-check': 'off', + 'jsdoc/require-returns-type': 'warn', + 'jsdoc/tag-lines': 'off', + 'no-shadow-restricted-names': 'off', + + /* --- Style --- */ + 'jsdoc/require-description': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns-description': 'off', + + /* --- custom classes and types --- */ + 'jsdoc/no-undefined-types': 'warn', // custom LuCI types + 'jsdoc/valid-types': 'warn', +} + +export const jsdoc_relaxed_rules = { + // 0: off, 1: warn, 2: error + /* --- JSDoc correctness --- */ + 'jsdoc/check-alignment': 'warn', + 'jsdoc/check-param-names': 'off', + 'jsdoc/check-tag-names': 'warn', + 'jsdoc/check-types': 'off', + 'jsdoc/no-defaults': 'off', + 'jsdoc/reject-any-type': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'warn', + 'jsdoc/require-returns': 'warn', + 'jsdoc/require-returns-check': 'off', + 'jsdoc/require-returns-type': 'warn', + 'jsdoc/tag-lines': 'off', + 'no-shadow-restricted-names': 'off', + + /* --- Style --- */ + 'jsdoc/require-description': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns-description': 'off', + + /* --- custom classes and types --- */ + 'jsdoc/no-undefined-types': 'off', // custom LuCI types + 'jsdoc/valid-types': 'off', +} export default defineConfig([ + globalIgnores([ + 'docs', + 'node_modules', + ]), + // Markdown { - files: ["**/*.json"], - ignores: ["package-lock.json"], - plugins: { json }, - language: "json/json", - extends: ["json/recommended"], + files: ["**/*.md"], + plugins: { + markdown, + }, + processor: "markdown/markdown", }, + // applies only to JavaScript blocks inside of Markdown files + { + files: ["**/*.md/*.js"], + rules: { + strict: "off", + }, + }, + // JSON files + { + files: ['**/*.json'], + ignores: ['package-lock.json'], + plugins: { json }, + language: 'json/json', + extends: ['json/recommended'], + rules: { + 'json/no-duplicate-keys': 'error', + }, + }, + // JavaScript files + { + files: ['**/*.js'], + language: '@/js', + plugins: { js }, + extends: ['js/recommended'], + linterOptions:{ + // silence warnings about inert // eslint-disable-next-line xxx + reportUnusedDisableDirectives: "off", + }, + languageOptions: { + sourceType: 'script', + ecmaVersion: 2026, // 2015 == ECMA6 + globals: { + ...globals.browser, + /* LuCI runtime / cbi exports */ + _: 'readonly', + N_: 'readonly', + L: 'readonly', + E: 'readonly', + TR: 'readonly', + cbi_d: 'readonly', + cbi_strings: 'readonly', + cbi_d_add: 'readonly', + cbi_d_check: 'readonly', + cbi_d_checkvalue: 'readonly', + cbi_d_update: 'readonly', + cbi_init: 'readonly', + cbi_update_table: 'readonly', + cbi_validate_form: 'readonly', + cbi_validate_field: 'readonly', + cbi_validate_named_section_add: 'readonly', + cbi_validate_reset: 'readonly', + cbi_row_swap: 'readonly', + cbi_tag_last: 'readonly', + cbi_submit: 'readonly', + cbi_dropdown_init: 'readonly', + isElem: 'readonly', + toElem: 'readonly', + matchesElem: 'readonly', + findParent: 'readonly', + sfh: 'readonly', + renderBadge: 'readonly', // found in theme templates + /* modules */ + baseclass: 'readonly', + dom: 'readonly', + firewall: 'readonly', + fwtool: 'readonly', + form: 'readonly', + fs: 'readonly', + network: 'readonly', + nettools: 'readonly', + poll: 'readonly', + random: 'readonly', + request: 'readonly', + session: 'readonly', + rpc: 'readonly', + uci: 'readonly', + ui: 'readonly', + uqr: 'readonly', + validation: 'readonly', + view: 'readonly', + widgets: 'readonly', + /* dockerman */ + dm2: 'readonly', + jsapi: 'readonly', + }, + parserOptions: { + ecmaFeatures: { + globalReturn: true, + } + }, + }, + rules: { // 0: off, 1: warn, 2: error + 'strict': 0, + 'no-prototype-builtins': 0, + 'no-empty': 0, + 'no-undef': 'warn', + 'no-unused-vars': ['off', { "caughtErrors": "none" }], + 'no-regex-spaces': 0, + 'no-control-regex': 0, + } + }, + { + extends: ['jsdoc/recommended'], // run jsdoc recommended rules + files: ['modules/luci-base/**/*.js'], // ... but only on these js files + plugins: { jsdoc }, + rules: { + /* use these settings when linting the checked out repo */ + // ...jsdoc_less_relaxed_rules + /* ... and use these settings for the repo (less noisy) */ + ...jsdoc_relaxed_rules + }, + } ]); - diff --git a/package.json b/package.json index 220b126af6..59e3d0fffa 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,16 @@ }, "devDependencies": { "@alphanull/jsdoc-vision-theme": "^1.2.2", + "@eslint/js": "^9.39.2", + "@eslint/json": "^1.0.1", + "@eslint/markdown": "^7.5.1", "clean-jsdoc-theme": "^4.3.0", + "eslint": "^9.39.2", + "eslint-formatter-gha": "^2.0.1", + "eslint-plugin-jsdoc": "^62.5.5", + "globals": "^17.3.0", "jaguarjs-jsdoc": "^1.1.0", - "jsdoc": "^4.0.5" + "jsdoc": "^4.0.5", + "neostandard": "^0.12.2" } }