diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 487026b619b..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,32 +0,0 @@ -coverage -.nyc_output -node_modules/ -.npm -.eslintcache -.env -.cache -.next - -dist -dist-pkg -.DS_Store -shell/utils/dynamic-importer.js -ksconfig.json -storybook-static/ -utils/dynamic-importer.js -shell/assets/fonts -assets/fonts -shell/pkg/import.js -shell/types/shell/index.d.ts -dist/ -coverage/ -build/ -public/ -pkg/**/node_modules -elemental-ui/ -kubewarden-ui/ -styleguide/src/stories/Example/ -storybook/src/stories/Example/ -cypress/dist/ -cypress/bin/ -cypress/template/ diff --git a/.eslintrc.default.js b/.eslintrc.default.js deleted file mode 100644 index ca497b9686d..00000000000 --- a/.eslintrc.default.js +++ /dev/null @@ -1,237 +0,0 @@ -module.exports = { - env: { - browser: true, - node: true - }, - globals: { - NodeJS: true, - Timer: true, - WebpackRequireContext: true - }, - plugins: [ - 'jest', - '@typescript-eslint', - 'local-rules' - ], - extends: [ - 'standard', - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - '@vue/standard', - '@vue/typescript/recommended', - 'plugin:vue/vue3-recommended', - 'plugin:cypress/recommended', - 'plugin:local-rules/all' - ], - rules: { - 'semi-spacing': 'off', - 'space-in-parens': 'off', - 'array-bracket-spacing': 'warn', - 'arrow-parens': 'warn', - 'arrow-spacing': [ - 'warn', - { - before: true, - after: true - } - ], - 'block-spacing': [ - 'warn', - 'always' - ], - 'brace-style': [ - 'warn', - '1tbs' - ], - 'comma-dangle': [ - 'warn', - 'only-multiline' - ], - 'comma-spacing': 'warn', - indent: [ - 'warn', - 2 - ], - 'keyword-spacing': 'warn', - 'newline-per-chained-call': [ - 'warn', - { ignoreChainWithDepth: 4 } - ], - 'no-trailing-spaces': 'warn', - 'func-call-spacing': [ - 'warn', - 'never' - ], - 'wrap-iife': 'off', - 'lines-between-class-members': [ - 'warn', - 'always', - { exceptAfterSingleLine: true } - ], - 'multiline-ternary': [ - 'warn', - 'never' - ], - 'no-whitespace-before-property': 'warn', - 'object-curly-spacing': [ - 'warn', - 'always' - ], - 'object-property-newline': 'warn', - 'object-shorthand': 'warn', - 'padded-blocks': [ - 'warn', - 'never' - ], - 'quote-props': 'warn', - 'rest-spread-spacing': 'warn', - semi: [ - 'warn', - 'always' - ], - 'space-before-function-paren': [ - 'warn', - 'never' - ], - 'space-infix-ops': 'warn', - 'spaced-comment': 'warn', - 'switch-colon-spacing': 'warn', - 'template-curly-spacing': [ - 'warn', - 'always' - ], - 'yield-star-spacing': [ - 'warn', - 'both' - ], - 'key-spacing': [ - 'warn', - { - align: { - beforeColon: false, - afterColon: true, - on: 'value', - mode: 'strict' - }, - multiLine: { - beforeColon: false, - afterColon: true - } - } - ], - 'object-curly-newline': [ - 'warn', - { - ObjectExpression: { - multiline: true, - minProperties: 3 - }, - ObjectPattern: { - multiline: true, - minProperties: 4 - }, - ImportDeclaration: { - multiline: true, - minProperties: 5 - }, - ExportDeclaration: { - multiline: true, - minProperties: 3 - } - } - ], - 'padding-line-between-statements': [ - 'warn', - { - blankLine: 'always', - prev: '*', - next: 'return' - }, - { - blankLine: 'always', - prev: 'function', - next: 'function' - }, - { - blankLine: 'always', - prev: [ - 'const', - 'let', - 'var' - ], - next: '*' - }, - { - blankLine: 'any', - prev: [ - 'const', - 'let', - 'var' - ], - next: [ - 'const', - 'let', - 'var' - ] - } - ], - quotes: [ - 'warn', - 'single', - { - avoidEscape: true, - allowTemplateLiterals: true - } - ], - 'space-unary-ops': [ - 'warn', - { - words: true, - nonwords: false - } - ], - 'vue/one-component-per-file': 'off', - 'vue/no-deprecated-slot-attribute': 'off', - 'vue/require-explicit-emits': 'error', - 'vue/v-on-event-hyphenation': 'off', - }, - overrides: [ - { - files: ['**/RcSeparator.vue'], - rules: { 'local-rules/no-hr-element': 'off' } - }, - { - files: [ - '**/*.{js,ts,vue}' - ], - rules: { - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/ban-types': 'off', - 'vue/require-toggle-inside-transition': 'off', // Introduced with new linting version 9.32.0 - } - }, - { - files: [ - '**/*.test.{js,ts}', - '**/__tests__/**/*.{js,ts}', - '**/__mocks__/**/*.{js,ts}' - ], - rules: { - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - 'jest/prefer-expect-assertions': 'off', - 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect', 'test*'] }] - }, - extends: [ - 'plugin:jest/recommended' - ] - }, - { - files: [ - '**/*.{js,vue}' - ], - rules: { '@typescript-eslint/explicit-module-boundary-types': 'off' } - } - ] -}; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 75cee114464..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,169 +0,0 @@ -module.exports = { - env: { - jest: true, - 'vue/setup-compiler-macros': true, - }, - extends: [ - '.eslintrc.default.js' - ], - rules: { - 'jest/no-commented-out-tests': 'off', - 'jest/no-disabled-tests': 'off', - 'dot-notation': 'off', - 'generator-star-spacing': 'off', - 'guard-for-in': 'off', - 'linebreak-style': 'off', - 'new-cap': 'off', - 'no-empty': 'off', - 'no-extra-boolean-cast': 'off', - 'no-new': 'off', - 'no-plusplus': 'off', - 'no-useless-escape': 'off', - 'nuxt/no-cjs-in-config': 'off', - strict: 'off', - 'unicorn/no-new-buffer': 'off', - 'vue/no-unused-components': 'warn', - curly: 'warn', - eqeqeq: 'warn', - 'implicit-arrow-linebreak': 'warn', - 'no-caller': 'warn', - 'no-cond-assign': [ - 'warn', - 'except-parens' - ], - 'no-console': 'warn', - 'no-debugger': 'warn', - 'no-eq-null': 'warn', - 'no-eval': 'warn', - 'no-undef': 'warn', - 'no-unused-vars': 'warn', - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': [ - 'error' - ], - 'prefer-arrow-callback': 'warn', - 'prefer-template': 'warn', - 'vue/order-in-components': 'off', - 'vue/no-lone-template': 'off', - 'vue/v-slot-style': 'off', - 'vue/component-tags-order': 'off', - 'vue/no-mutating-props': 'off', - '@typescript-eslint/no-unused-vars': 'off', - 'array-callback-return': 'off', - 'import/order': 'off', - 'import/no-named-as-default': 'off', - 'vue/multi-word-component-names': 'off', - 'vue/no-reserved-component-names': 'off', - 'vue/no-useless-template-attributes': 'off', - 'vue/attribute-hyphenation': 'off', - 'vue/valid-next-tick': 'off', - 'vue/no-computed-properties-in-data': 'off', - 'vue/no-side-effects-in-computed-properties': 'off', - '@typescript-eslint/no-var-requires': 'off', - 'vue/one-component-per-file': 'off', - 'vue/no-deprecated-slot-attribute': 'off', - 'vue/v-on-event-hyphenation': 'off', - }, - overrides: [ - { - files: ['*.d.ts'], - rules: { - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'warn', - }, - }, - { - files: [ - '*.js' - ], - rules: { - 'prefer-regex-literals': 'off', - 'vue/component-definition-name-casing': 'off', - 'no-unreachable-loop': 'off', - 'computed-property-spacing': 'off' - }, - }, - { - files: [ - 'docusaurus/**/*.{js,ts}' - ], - rules: { 'no-use-before-define': 'off' } - }, - { - files: [ - '**/*.vue' - ], - rules: { - 'vue/no-v-html': 'error', - 'vue/html-indent': [ - 'error', - 2 - ], - 'vue/html-closing-bracket-newline': [ - 'error', - { - singleline: 'never', - multiline: 'always' - } - ], - 'vue/html-closing-bracket-spacing': 2, - 'vue/html-end-tags': 2, - 'vue/html-quotes': 2, - 'vue/html-self-closing': [ - 'error', - { - html: { - void: 'never', - normal: 'always', - component: 'always' - }, - svg: 'always', - math: 'always' - } - ], - 'vue/max-attributes-per-line': [ - 'error', - { - singleline: { max: 1 }, - multiline: { max: 1 } - } - ] - } - }, - { - files: [ - '**/shell/utils/**/*.{js,ts}', - '**/shell/scripts/**/*.{js,ts}' - ], - rules: { - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-var-requires': 'off' - } - }, - { - files: [ - '**/*.{js,ts,vue}' - ], - rules: { - '@typescript-eslint/no-this-alias': 'off', - '@typescript-eslint/no-explicit-any': 'off' - } - }, - { - files: [ - '**/{harvester,harvester-manager}/**/*.{js,ts,vue}' - ], - rules: { - '@typescript-eslint/explicit-module-boundary-types': 'off', - 'vue/html-self-closing': 'off', - 'vue/no-v-html': 'error' - } - }, - { - files: [ - '**/po/**/*.{js,ts,vue}' - ], - rules: { '@typescript-eslint/explicit-module-boundary-types': 'off' } - } - ] -}; diff --git a/.gitignore b/.gitignore index 54b5ee10826..3a3993a35cf 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,6 @@ scripts/standalone/node # GitHub actions scripts output codecov + +# @rancher/shell on-demand legacy ESLint toolchain (installed by scripts/lint) +shell/eslint-legacy/node_modules/ diff --git a/creators/extension/app/files/.eslintignore b/creators/extension/app/files/.eslintignore deleted file mode 100644 index 8e610d1e839..00000000000 --- a/creators/extension/app/files/.eslintignore +++ /dev/null @@ -1,16 +0,0 @@ -assets/fonts -coverage -.nyc_output -node_modules/ -.npm -.eslintcache -.env -.cache -.next -dist -dist-pkg -.DS_Store -dynamic-importer.js -ksconfig.json -shell/utils/dynamic-importer.js -shell/assets/fonts diff --git a/creators/extension/app/files/.eslintrc.js b/creators/extension/app/files/.eslintrc.js deleted file mode 100644 index e8ad3a10f89..00000000000 --- a/creators/extension/app/files/.eslintrc.js +++ /dev/null @@ -1,243 +0,0 @@ -module.exports = { - root: true, - env: { - browser: true, - node: true - }, - globals: { - NodeJS: true, - Timer: true - }, - extends: [ - 'standard', - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - '@vue/standard', - '@vue/typescript/recommended', - 'plugin:vue/vue3-recommended', - 'plugin:cypress/recommended' - ], - rules: { - 'dot-notation': 'off', - 'generator-star-spacing': 'off', - 'guard-for-in': 'off', - 'linebreak-style': 'off', - 'new-cap': 'off', - 'no-empty': 'off', - 'no-extra-boolean-cast': 'off', - 'no-new': 'off', - 'no-plusplus': 'off', - 'no-useless-escape': 'off', - 'semi-spacing': 'off', - 'space-in-parens': 'off', - strict: 'off', - 'unicorn/no-new-buffer': 'off', - 'vue/html-self-closing': 'off', - 'vue/no-unused-components': 'warn', - 'vue/no-v-html': 'error', - 'wrap-iife': 'off', - 'array-bracket-spacing': 'warn', - 'arrow-parens': 'warn', - 'arrow-spacing': [ - 'warn', - { - before: true, - after: true - } - ], - 'block-spacing': [ - 'warn', - 'always' - ], - 'brace-style': [ - 'warn', - '1tbs' - ], - 'comma-dangle': [ - 'warn', - 'only-multiline' - ], - 'comma-spacing': 'warn', - curly: 'warn', - eqeqeq: 'warn', - 'func-call-spacing': [ - 'warn', - 'never' - ], - 'implicit-arrow-linebreak': 'warn', - indent: [ - 'warn', - 2 - ], - 'keyword-spacing': 'warn', - 'lines-between-class-members': [ - 'warn', - 'always', - { exceptAfterSingleLine: true } - ], - 'multiline-ternary': [ - 'warn', - 'never' - ], - 'newline-per-chained-call': [ - 'warn', - { ignoreChainWithDepth: 4 } - ], - 'no-caller': 'warn', - 'no-cond-assign': [ - 'warn', - 'except-parens' - ], - 'no-console': 'warn', - 'no-debugger': 'warn', - 'no-eq-null': 'warn', - 'no-eval': 'warn', - 'no-trailing-spaces': 'warn', - 'no-undef': 'warn', - 'no-unused-vars': 'warn', - 'no-whitespace-before-property': 'warn', - 'object-curly-spacing': [ - 'warn', - 'always' - ], - 'object-property-newline': 'warn', - 'object-shorthand': 'warn', - 'padded-blocks': [ - 'warn', - 'never' - ], - 'prefer-arrow-callback': 'warn', - 'prefer-template': 'warn', - 'quote-props': 'warn', - 'rest-spread-spacing': 'warn', - semi: [ - 'warn', - 'always' - ], - 'space-before-function-paren': [ - 'warn', - 'never' - ], - 'space-infix-ops': 'warn', - 'spaced-comment': 'warn', - 'switch-colon-spacing': 'warn', - 'template-curly-spacing': [ - 'warn', - 'always' - ], - 'yield-star-spacing': [ - 'warn', - 'both' - ], - 'key-spacing': [ - 'warn', - { - align: { - beforeColon: false, - afterColon: true, - on: 'value', - mode: 'minimum' - }, - multiLine: { - beforeColon: false, - afterColon: true - } - } - ], - 'object-curly-newline': [ - 'warn', - { - ObjectExpression: { - multiline: true, - minProperties: 3 - }, - ObjectPattern: { - multiline: true, - minProperties: 4 - }, - ImportDeclaration: { - multiline: true, - minProperties: 5 - }, - ExportDeclaration: { - multiline: true, - minProperties: 3 - } - } - ], - 'padding-line-between-statements': [ - 'warn', - { - blankLine: 'always', - prev: '*', - next: 'return' - }, - { - blankLine: 'always', - prev: 'function', - next: 'function' - }, - { - blankLine: 'always', - prev: [ - 'const', - 'let', - 'var' - ], - next: '*' - }, - { - blankLine: 'any', - prev: [ - 'const', - 'let', - 'var' - ], - next: [ - 'const', - 'let', - 'var' - ] - } - ], - quotes: [ - 'warn', - 'single', - { - avoidEscape: true, - allowTemplateLiterals: true - } - ], - 'space-unary-ops': [ - 'warn', - { - words: true, - nonwords: false - } - ], - 'vue/order-in-components': 'off', - 'vue/no-lone-template': 'off', - 'vue/v-slot-style': 'off', - 'vue/component-tags-order': 'off', - 'vue/no-mutating-props': 'off', - '@typescript-eslint/no-unused-vars': 'off', - 'array-callback-return': 'off', - 'vue/one-component-per-file': 'off', - 'vue/no-deprecated-slot-attribute': 'off', - 'vue/require-explicit-emits': 'off', - 'vue/v-on-event-hyphenation': 'off' - }, - overrides: [ - { - files: [ - '*.js' - ], - rules: { - 'prefer-regex-literals': 'off', - 'vue/component-definition-name-casing': 'off', - 'no-unreachable-loop': 'off', - 'computed-property-spacing': 'off' - } - } - ] -}; diff --git a/creators/extension/app/files/.vscode/settings.json b/creators/extension/app/files/.vscode/settings.json index 3124fc15a7f..3e5d7596722 100644 --- a/creators/extension/app/files/.vscode/settings.json +++ b/creators/extension/app/files/.vscode/settings.json @@ -5,8 +5,7 @@ ".drone.yml": true, ".editorconfig": true, ".eslintcache": true, - ".eslintignore": true, - ".eslintrc.js": true, + "eslint.config.mjs": true, ".gitignore": true, ".nyc_output": true, ".vscode": true, diff --git a/creators/extension/app/files/eslint.config.mjs b/creators/extension/app/files/eslint.config.mjs new file mode 100644 index 00000000000..3585ed67f18 --- /dev/null +++ b/creators/extension/app/files/eslint.config.mjs @@ -0,0 +1,22 @@ +/** + * Flat ESLint config for a Rancher UI extension. + * + * The shared ruleset + eslint toolchain (eslint, neostandard, typescript-eslint, eslint-plugin-vue, + * ...) are provided by `@rancher/shell`, so this file just re-exports the shared config and adds any + * project-specific overrides. + * + * Linting runs through `@rancher/shell`'s launcher (see the `lint` script in package.json), which + * selects ESLint 10 for this flat config. Extensions that still use a legacy `.eslintrc.*` file are + * instead linted with the shipped ESLint 7 toolchain — see the extension ESLint documentation. + */ +import shellConfig from '@rancher/shell/eslint.config.base.mjs'; + +export default [ + ...shellConfig, + + // Add project-specific overrides below, e.g.: + // { + // files: ['**/*.vue'], + // rules: { 'vue/no-v-html': 'off' }, + // }, +]; diff --git a/creators/extension/app/init b/creators/extension/app/init index 7397a8b0882..4046bae8fe1 100755 --- a/creators/extension/app/init +++ b/creators/extension/app/init @@ -7,6 +7,7 @@ const fs = require('fs-extra'); const targets = { dev: 'NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve', build: './node_modules/.bin/vue-cli-service build', + lint: './node_modules/@rancher/shell/scripts/lint --ext .js,.ts,.vue .', clean: './node_modules/@rancher/shell/scripts/clean' }; @@ -14,8 +15,7 @@ const files = [ 'tsconfig.json', 'vue.config.js', 'gitignore', - '.eslintignore', - '.eslintrc.js', + 'eslint.config.mjs', '.nvmrc', '.yarnrc', 'babel.config.js', diff --git a/docusaurus/docs/extensions/linting.md b/docusaurus/docs/extensions/linting.md new file mode 100644 index 00000000000..56181a8a16e --- /dev/null +++ b/docusaurus/docs/extensions/linting.md @@ -0,0 +1,71 @@ +# Linting (ESLint) + +`@rancher/shell` provides the ESLint toolchain that extensions use. As of Shell v3.1, that toolchain moved from ESLint 7 (legacy `.eslintrc.*` config) to **ESLint 10 with flat config** (`eslint.config.mjs`). + +ESLint 10 cannot read a legacy `.eslintrc.*` file at all, so this would normally be a breaking change for every published extension. To avoid that, `@rancher/shell` ships **both** ESLint toolchains and selects the right one for your project automatically. + +## How selection works + +Run linting through the launcher that `@rancher/shell` provides, wired up as the `lint` script: + +```json +{ + "scripts": { + "lint": "./node_modules/@rancher/shell/scripts/lint --ext .js,.ts,.vue ." + } +} +``` + +The launcher looks at which config file exists in your extension and picks the matching ESLint: + +| Config file in your extension | ESLint used | +|---|---| +| `eslint.config.{js,mjs,cjs}` (flat) | ESLint 10 | +| `.eslintrc.{js,cjs,json,yml,yaml}` (legacy) | ESLint 7 | + +So an extension that still uses a legacy `.eslintrc.*` keeps linting on ESLint 7 with no changes, and an extension that has migrated to a flat `eslint.config.mjs` is linted with ESLint 10. The ESLint 7 toolchain is self-contained and installed on first use (it needs network access and a writable `node_modules` the first time you lint a legacy project). + +## New extensions + +Extensions scaffolded with the creator already include: + +- an `eslint.config.mjs` that re-exports the shared Rancher config, and +- the `lint` script shown above. + +The generated `eslint.config.mjs` is only a few lines — the ruleset lives in `@rancher/shell`: + +```js +import shellConfig from '@rancher/shell/eslint.config.base.mjs'; + +export default [ + ...shellConfig, + + // Add project-specific overrides below. +]; +``` + +## Existing extensions + +You do **not** have to change anything to keep `yarn lint` / CI working — the launcher keeps your legacy `.eslintrc.*` on ESLint 7. Make sure your `lint` script points at the launcher (as above) so the selection happens. + +When you are ready to move to ESLint 10 / flat config: + +1. Delete `.eslintrc.*` and `.eslintignore`. +2. Add an `eslint.config.mjs` that re-exports the shared config (see above), moving any custom rules into the exported array. + +This is the same setup new extensions are scaffolded with, so you can copy the generated `eslint.config.mjs` as a starting point. + +## Editor (IDE) integration + +The launcher only governs command-line and CI linting. Editor ESLint integrations (VS Code, JetBrains) load the `eslint` module directly and do not go through the launcher, so an editor resolves a single ESLint version. + +- **Flat-config extensions** work out of the box (the resolved ESLint is 10). +- **Legacy-config extensions** that want in-editor linting can point the editor at the shipped ESLint 7 toolchain, e.g. in `.vscode/settings.json`: + + ```json + { + "eslint.nodePath": "node_modules/@rancher/shell/eslint-legacy/node_modules" + } + ``` + + (Run `yarn lint` once first so the legacy toolchain is installed.) Migrating to flat config removes the need for any editor-specific setting. diff --git a/docusaurus/extensionSidebar.js b/docusaurus/extensionSidebar.js index 623e7a72a27..67bda61ce32 100644 --- a/docusaurus/extensionSidebar.js +++ b/docusaurus/extensionSidebar.js @@ -86,6 +86,7 @@ const sidebars = { 'folder-structure', 'configuration', 'unit-testing', + 'linting', { type: 'category', label: 'Performance', diff --git a/eslint-plugin-local-rules/no-hr-element.js b/eslint-plugin-local-rules/no-hr-element.js index 2abb2f278f8..e7387df9e13 100644 --- a/eslint-plugin-local-rules/no-hr-element.js +++ b/eslint-plugin-local-rules/no-hr-element.js @@ -1,4 +1,8 @@ -const vueUtils = require('eslint-plugin-vue/lib/utils'); +// eslint-plugin-vue v10 ships its internals under `dist/` (was `lib/`) and wraps the +// CommonJS export in a `.default`. `defineTemplateBodyVisitor` has no public entry point, +// so reach into `dist/utils` — the v10 successor to the old `lib/utils` path. +const vueUtilsModule = require('eslint-plugin-vue/dist/utils'); +const vueUtils = vueUtilsModule.default || vueUtilsModule; module.exports = { meta: { diff --git a/eslint-plugin-local-rules/v-clean-tooltip.js b/eslint-plugin-local-rules/v-clean-tooltip.js index 5f43105a5f7..421fdd3a848 100644 --- a/eslint-plugin-local-rules/v-clean-tooltip.js +++ b/eslint-plugin-local-rules/v-clean-tooltip.js @@ -1,6 +1,3 @@ -// Currently loading these rules with the --rulesdir argument. In the future we could make use of `eslint-plugin-local-rules`. -const vueUtils = require('eslint-plugin-vue/lib/utils'); - module.exports = { meta: { type: 'problem', @@ -8,7 +5,12 @@ module.exports = { schema: [], }, create(context) { - return vueUtils.defineTemplateBodyVisitor(context, { + // `vue-eslint-parser` exposes `defineTemplateBodyVisitor` via parserServices at runtime. + // (eslint-plugin-vue v10 removed the `eslint-plugin-vue/lib/utils` re-export this used to use.) + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const parserServices = sourceCode.parserServices ?? context.parserServices; + + return parserServices.defineTemplateBodyVisitor({ VAttribute(node) { // v-tooltip is a VDirectiveKey if (node?.key?.type !== 'VDirectiveKey') { diff --git a/eslint.config.base.mjs b/eslint.config.base.mjs new file mode 100644 index 00000000000..b435bf0f596 --- /dev/null +++ b/eslint.config.base.mjs @@ -0,0 +1,263 @@ +/** + * Shared flat-config base — the flat-config successor to the old `.eslintrc.default.js`. + * + * Imported by the root `eslint.config.mjs` and `pkg/rancher-components/eslint.config.mjs` + * so both share one base (mirroring the legacy `extends: ['../../.eslintrc.default.js']`). + * + * `standard` + `@vue/standard` (capped at eslint 8, no flat release) are replaced by their + * flat successor `neostandard`. The import rules that `eslint-config-standard` used to + * provide are re-added explicitly, since `neostandard` drops the `import` plugin. + */ +import js from '@eslint/js'; +import globals from 'globals'; +import neostandard from 'neostandard'; +import tseslint from 'typescript-eslint'; +import pluginVue from 'eslint-plugin-vue'; +import pluginCypress from 'eslint-plugin-cypress/flat'; +import pluginJest from 'eslint-plugin-jest'; +import pluginImport from 'eslint-plugin-import'; +import pluginNode from 'eslint-plugin-node'; +import vueParser from 'vue-eslint-parser'; +import localRules from 'eslint-plugin-local-rules'; + +// The `standard` import rules that `neostandard` no longer pulls in. +const standardImportRules = { + 'import/export': 'error', + 'import/first': 'error', + 'import/no-absolute-path': ['error', { esmodule: true, commonjs: true, amd: false }], + 'import/no-duplicates': 'error', + 'import/no-named-default': 'error', + 'import/no-webpack-loader-syntax': 'error', +}; + +export const testFiles = [ + '**/*.test.{js,ts}', + '**/__tests__/**/*.{js,ts}', + '**/__mocks__/**/*.{js,ts}', +]; + +/** + * Base flat config (was `.eslintrc.default.js`). Order mirrors the legacy `extends` chain: + * eslint:recommended -> standard(+@vue/standard via neostandard) -> + * @typescript-eslint/recommended -> vue/vue3-recommended -> cypress/recommended -> + * local-rules/all, then the custom rule block + per-file overrides. + */ +export const eslintConfigBase = [ + // Match the legacy lint scope. The old scripts used `--ext .js,.ts,.vue`, and eslintrc + // ignored dotfiles/dot-directories by default — flat config does neither, so replicate: + // - only lint .js/.ts/.vue (ignore .tsx/.jsx/.mjs/.cjs, incl. these config files), + // - ignore dotfiles & dot-directories (.github, .storybook, .vscode, ...). + { + ignores: [ + '**/*.tsx', + '**/*.jsx', + '**/*.mjs', + '**/*.cjs', + '**/.*', + '**/.*/**', + ], + }, + + // Flat config defaults `reportUnusedDisableDirectives` to "warn"; the legacy eslintrc + // setup left it off. Keep it off to preserve the pre-migration behaviour. + { linterOptions: { reportUnusedDisableDirectives: 'off' } }, + + // eslint:recommended + js.configs.recommended, + + // standard + @vue/standard (flat successor). `noStyle: true` keeps neostandard's quality + // rules but omits its `@stylistic/*` layer: those rules are TypeScript-aware and would + // flag type-syntax (type-literal braces, `interface X{`, parens around `as` casts) that + // the legacy eslint-config-standard *core* formatting rules never checked. Formatting is + // instead provided below by the same core rules the legacy config used (JS-only) — an + // exact behavioural match that keeps the codebase green without source changes. + ...neostandard({ ts: true, noStyle: true, noJsx: true }), + + // @typescript-eslint/recommended + ...tseslint.configs.recommended, + + // vue/vue3-recommended (flat naming: `flat/recommended` === Vue 3 recommended) + ...pluginVue.configs['flat/recommended'], + + // cypress/recommended + pluginCypress.configs.recommended, + + // Plugin registration (so namespaces resolve, incl. inline `eslint-disable node/*` + // directives that survive from before standard switched `node` -> `n`), language + // options and the parser the dropped `@vue/typescript/recommended` used to configure. + { + plugins: { + jest: pluginJest, + import: pluginImport, + node: pluginNode, + 'local-rules': localRules, + }, + languageOptions: { + parser: vueParser, + ecmaVersion: 2020, + sourceType: 'module', + parserOptions: { + parser: tseslint.parser, + extraFileExtensions: ['.vue'], + }, + globals: { + ...globals.browser, + ...globals.node, + NodeJS: 'readonly', + Timer: 'readonly', + WebpackRequireContext: 'readonly', + }, + }, + }, + + // local-rules/all + { + rules: { + 'local-rules/v-clean-tooltip': 'error', + 'local-rules/no-hr-element': 'error', + }, + }, + + // --- custom rule block (ported verbatim from `.eslintrc.default.js`) --- + { + rules: { + ...standardImportRules, + 'import/order': 'off', + 'import/no-named-as-default': 'off', + + // --- ESLint v9 / typescript-eslint v8 upgrade compatibility --- + // Disabled (config-only, no source changes) to preserve the pre-upgrade green state. + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-wrapper-object-types': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + 'no-unsafe-optional-chaining': 'off', + 'no-import-assign': 'off', + 'no-constant-binary-expression': 'off', + 'n/no-deprecated-api': 'off', + 'n/no-callback-literal': 'off', + 'cypress/unsafe-to-chain-command': 'off', + camelcase: 'off', + // --- end upgrade compatibility block --- + + // --- ESLint 10 / eslint-plugin-vue 10 ruleset churn --- + // Rules newly added to recommended by the eslint 9->10 + vue 9->10 bump. Disabled + // (config-only, no source changes) to preserve the green state, same as the block above. + 'no-useless-assignment': 'off', // new in eslint 10 recommended + 'preserve-caught-error': 'off', // new in eslint 10 recommended + 'vue/no-required-prop-with-default': 'off', // new in eslint-plugin-vue 10 recommended + 'vue/require-default-prop': 'off', // re-fires under eslint-plugin-vue 10 + // --- end eslint 10 / vue 10 churn block --- + + // Core formatting rules — values captured verbatim from the pre-migration + // (`eslint-config-standard` + legacy overrides) resolved config. Core rules are + // JS-only, exactly matching the previous behaviour (see `noStyle` note above). + 'array-bracket-spacing': ['warn', 'never'], + 'arrow-parens': ['warn'], + 'arrow-spacing': ['warn', { before: true, after: true }], + 'block-spacing': ['warn', 'always'], + 'brace-style': ['warn', '1tbs'], + 'comma-dangle': ['warn', 'only-multiline'], + 'comma-spacing': ['warn', { before: false, after: true }], + 'comma-style': ['error', 'last'], + 'computed-property-spacing': ['error', 'never', { enforceForClassMembers: true }], + 'dot-location': ['error', 'property'], + 'eol-last': ['error'], + 'func-call-spacing': ['warn', 'never'], + 'keyword-spacing': ['warn', { before: true, after: true }], + 'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }], + 'multiline-ternary': ['warn', 'never'], + 'new-parens': ['error'], + 'newline-per-chained-call': ['warn', { ignoreChainWithDepth: 4 }], + 'no-extra-parens': ['error', 'functions'], + 'no-floating-decimal': ['error'], + 'no-mixed-operators': ['error', { + allowSamePrecedence: true, + groups: [['==', '!=', '===', '!==', '>', '>=', '<', '<='], ['&&', '||'], ['in', 'instanceof']], + }], + 'no-mixed-spaces-and-tabs': ['error'], + 'no-multi-spaces': ['error'], + 'no-tabs': ['error'], + 'no-trailing-spaces': ['warn'], + 'no-whitespace-before-property': ['warn'], + 'object-curly-newline': ['warn', { + ObjectExpression: { multiline: true, minProperties: 3 }, + ObjectPattern: { multiline: true, minProperties: 4 }, + ImportDeclaration: { multiline: true, minProperties: 5 }, + ExportDeclaration: { multiline: true, minProperties: 3 }, + }], + 'object-curly-spacing': ['warn', 'always'], + 'object-property-newline': ['warn', { allowAllPropertiesOnSameLine: false, allowMultiplePropertiesPerLine: true }], + 'object-shorthand': 'warn', + 'operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before', '|>': 'before' } }], + 'padded-blocks': ['warn', 'never'], + 'padding-line-between-statements': ['warn', + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: 'function', next: 'function' }, + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + ], + 'quote-props': ['warn', 'as-needed'], + quotes: ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }], + 'rest-spread-spacing': ['warn', 'never'], + 'space-before-blocks': ['error', 'always'], + 'space-before-function-paren': ['warn', 'never'], + 'space-infix-ops': ['warn'], + 'space-unary-ops': ['warn', { words: true, nonwords: false }], + 'spaced-comment': ['warn', 'always', { + line: { markers: ['*package', '!', '/', ',', '='] }, + block: { balanced: true, markers: ['*package', '!', ',', ':', '::', 'flow-include'], exceptions: ['*'] }, + }], + 'switch-colon-spacing': ['warn'], + 'template-curly-spacing': ['warn', 'always'], + 'template-tag-spacing': ['error', 'never'], + 'yield-star-spacing': ['warn', 'both'], + + 'vue/one-component-per-file': 'off', + 'vue/no-deprecated-slot-attribute': 'off', + 'vue/require-explicit-emits': 'error', + 'vue/v-on-event-hyphenation': 'off', + }, + }, + + // override: RcSeparator.vue is the sanctioned wrapper around
(was `.eslintrc.default.js` overrides[0]) + { + files: ['**/RcSeparator.vue'], + rules: { 'local-rules/no-hr-element': 'off' }, + }, + + // override: **/*.{js,ts,vue} (was `.eslintrc.default.js` overrides[1]) + { + files: ['**/*.{js,ts,vue}'], + rules: { + '@typescript-eslint/no-empty-function': 'off', + // `@typescript-eslint/ban-types` was split in ts-eslint v8; disable successors. + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/no-restricted-types': 'off', + 'vue/require-toggle-inside-transition': 'off', + }, + }, + + // override: test files — jest recommended + jest globals (was overrides[2]) + { + files: testFiles, + ...pluginJest.configs['flat/recommended'], + languageOptions: { globals: { ...globals.jest } }, + rules: { + ...pluginJest.configs['flat/recommended'].rules, + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'jest/prefer-expect-assertions': 'off', + 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect', 'test*'] }], + }, + }, + + // override: **/*.{js,vue} (was overrides[3]) + { + files: ['**/*.{js,vue}'], + rules: { '@typescript-eslint/explicit-module-boundary-types': 'off' }, + }, +]; + +export default eslintConfigBase; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..b84997cb53c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,209 @@ +/** + * Root flat config — the flat-config successor to `.eslintrc.js` (+ `.eslintrc.default.js` + * via the shared `eslintConfigBase`). Ignores are ported from the old `.eslintignore`. + * + * Behaviour-preserving migration: the effective ruleset matches the previous + * eslintrc setup. A handful of `'off'` rule references were dropped because their plugins + * are no longer registered (`nuxt/*`, `unicorn/*`) or the rule was removed in + * typescript-eslint v8 (`@typescript-eslint/no-var-requires`); being `'off'`, dropping them + * changes nothing. + */ +import globals from 'globals'; +import { eslintConfigBase } from './eslint.config.base.mjs'; + +// Vue `