UNPKG

@nitra/eslint-config

Version:

An ESLint shareable config for projects using Vue and Node

822 lines (778 loc) 35.6 kB
import { readFileSync } from 'node:fs' import { fixupPluginRules } from '@eslint/compat' import js from '@eslint/js' import graphqlEslintPlugin from '@graphql-eslint/eslint-plugin' import microsoftSdl from '@microsoft/eslint-plugin-sdl' import { mergeProcessors } from 'eslint-merge-processors' import { flatConfigs as importXFlatConfigs } from 'eslint-plugin-import-x' import jsdocPlugin from 'eslint-plugin-jsdoc' import { configs as jsoncConfigs } from 'eslint-plugin-jsonc' import markdownPlugin from '@eslint/markdown' import nodePlugin from 'eslint-plugin-n' import noUnsanitized from 'eslint-plugin-no-unsanitized' import oxlint from 'eslint-plugin-oxlint' import securityPlugin from 'eslint-plugin-security' import sonarjsPlugin from 'eslint-plugin-sonarjs' import unicornPlugin from 'eslint-plugin-unicorn' import vuePlugin from 'eslint-plugin-vue' import { configs as ymlConfigs } from 'eslint-plugin-yml' import processorVueBlocks from 'eslint-processor-vue-blocks' import globals from 'globals' import vueEslintParser from 'vue-eslint-parser' import { graphqlCodefileProcessor, graphqlParserWithConfigAnchor } from './graphql-eslint-anchor.js' /** Витягнуті graphql-eslint документи: gql під `node_modules` не обробляються (sibling-фільтр у `graphql-eslint-anchor` уже прибирає залежності з унікальності імен; patterns `ignores` скорочують шум і час). */ const GRAPHQL_EXTRACTED_IGNORES = ['**/node_modules/**'] /** Glob-патерни файлів для eslint-plugin-unicorn (JS-подібні джерела; без сирих YAML/Markdown). */ const UNICORN_FILES = ['**/*.{js,mjs,cjs,vue}'] /** * Винятки для `@microsoft/sdl/no-insecure-url`: усі дефолти з `eslint-plugin-sdl/lib/rules/no-insecure-url.js` * (`DEFAULT_EXCEPTIONS`) плюс локальні HTTP dev-хости — без заміни дефолтного списку цілком (опція `exceptions` його замінює). * @see https://github.com/microsoft/eslint-plugin-sdl/blob/main/lib/rules/no-insecure-url.js */ const SDL_NO_INSECURE_URL_EXCEPTIONS = [ String.raw`^http:(\/\/|\u002f\u002f)schemas\.microsoft\.com(\/\/|\u002f\u002f)?.*`, String.raw`^http:(\/\/|\u002f\u002f)schemas\.openxmlformats\.org(\/\/|\u002f\u002f)?.*`, String.raw`^http:(\/|\u002f){2}localhost(:|\/|\u002f)*`, String.raw`^http:(\/\/)www\.w3\.org\/1999\/xhtml`, String.raw`^http:(\/\/)www\.w3\.org\/2000\/svg`, String.raw`^http:\/\/127\.0\.0\.1(:\d+)?(\/|$)`, String.raw`^http:\/\/0\.0\.0\.0(:\d+)?(\/|$)` ] /** * Заборона `for...in`: підказує рефакторинг на `for...of` з `Object.entries/keys/values` чи прямою ітерацією масиву. * Разом із цим зникає потреба в `Object.hasOwn`-guard. */ const NO_FOR_IN_RESTRICTED_SYNTAX = [ 'error', { selector: 'ForInStatement', message: 'Use for...of with Object.entries/keys/values or iterate arrays directly' } ] /** Узгоджені опції `no-unused-vars` для `.js` і Vue SFC (`eslint-plugin-oxlint` не вимикає це правило для `.vue`, тож oxlint CLI його там не покриває). */ const NO_UNUSED_VARS_RULE = ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }] /** Правила з `eslint:recommended` + `no-unused-vars` для js/mjs/cjs (однаковий набір; `sourceType` задається в окремих блоках за розширенням). */ const ESLINT_RECOMMENDED_JS_LIKE_RULES = { ...js.configs.recommended.rules, 'no-unused-vars': NO_UNUSED_VARS_RULE } /** * Частина ядра `eslint:recommended`, яку не вимикає `eslint-plugin-oxlint` flat/recommended (oxlint не заявляє заміну * цих правил у ESLint) і яка не ввімкнена в oxlint за кореневим `.oxlintrc.json` — для `<script>` у `.vue` їх * лишаємо на ESLint після oxlint. Решту з recommended для SFC покриває oxlint CLI з вашого `.oxlintrc.json`. */ const VUE_SCRIPT_ESLINT_RECOMMENDED_GAP_RULES = { 'getter-return': 'error', 'no-dupe-args': 'error', 'no-octal': 'error', 'no-undef': 'error', 'no-unreachable': 'error', 'no-useless-assignment': 'error', 'preserve-caught-error': 'error', 'no-unused-vars': NO_UNUSED_VARS_RULE } /** * Лише віртуальні GraphQL-документи з процесора (gql у `.js`/`.vue`): шляхи виду * `…/file.js/0_document.graphql` або `…/Comp.vue/…/0_document.graphql`. Фізичні `*.graphql` у проєкті не * потрапляють сюди — їх ESLint з цим пакетом не аналізує graphql-eslint. */ const GRAPHQL_EXTRACTED_DOCUMENT_FILES = [ '**/*.vue/**/*.graphql', '**/*.js/*.graphql', '**/*.mjs/*.graphql', '**/*.cjs/*.graphql' ] /** * Обгортає правило graphql-eslint і логує runtime-падіння visitor callbacks, не змінюючи поведінку (після логу помилка знову викидається). * @param {import('eslint').Rule.RuleModule} ruleImpl оригінальна імплементація правила * @param {string} ruleName ім'я правила для debug payload * @returns {import('eslint').Rule.RuleModule} обгорнуте правило з runtime-логуванням */ function instrumentGraphqlRuleForDebug(ruleImpl, ruleName) { if (!ruleImpl?.create) { return ruleImpl } return { ...ruleImpl, create(context) { const visitors = ruleImpl.create(context) return Object.fromEntries( Object.entries(visitors).map(([selector, callback]) => [ selector, /** * Обгортка visitor callback з guard для відомого падіння no-deprecated. * @param {unknown[]} args аргументи visitor від graphql-eslint * @returns {unknown} результат оригінального callback (або `undefined` для відомого crash-case) */ (...args) => { try { return callback(...args) } catch (error) { const knownNoDeprecatedCrash = ruleName === '@graphql-eslint/no-deprecated' && selector === 'ObjectValue' && error instanceof TypeError && error.message.includes('deprecationReason') if (knownNoDeprecatedCrash) { return } throw error } } ]) ) } } } const graphqlEslintPluginWithDebug = { ...graphqlEslintPlugin, rules: { ...graphqlEslintPlugin.rules, 'no-deprecated': instrumentGraphqlRuleForDebug( graphqlEslintPlugin.rules['no-deprecated'], '@graphql-eslint/no-deprecated' ) } } /** * `@graphql-eslint/naming-convention` з operations-recommended плюс сумісність із Hasura: * аргументи на кшталт `_set`, `_eq` починаються з символу підкреслення — без `allowLeadingUnderscore` * правило глобально лає будь-який `Name`, що починається з `_` (не лише camelCase для змінних). * @see https://the-guild.dev/graphql/eslint/rules/naming-convention */ const graphqlNamingConventionHasuraFriendly = [ 'error', { allowLeadingUnderscore: true, VariableDefinition: 'camelCase', OperationDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'], forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'] }, FragmentDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Fragment'], forbiddenSuffixes: ['Fragment'] } } ] /** * Правила для витягнутих GraphQL-блоків: пресет operations-recommended (поля, типи, змінні). * Схема та documents підтягуються через graphql-config (файл налаштувань у корені пакета з операціями, зазвичай YAML). */ const graphqlExtractedDocumentLint = { files: GRAPHQL_EXTRACTED_DOCUMENT_FILES, ignores: GRAPHQL_EXTRACTED_IGNORES, languageOptions: { parser: graphqlParserWithConfigAnchor }, plugins: { // ESLint 10: graphql-eslint ще викликає context.getSourceCode() у частині правил — fixup з @eslint/compat. '@graphql-eslint': fixupPluginRules(graphqlEslintPluginWithDebug) }, rules: { ...graphqlEslintPlugin.configs['flat/operations-recommended'].rules, '@graphql-eslint/naming-convention': graphqlNamingConventionHasuraFriendly, '@graphql-eslint/no-deprecated': 'error' } } /** * Вимкнення unique-operation-name лише для віртуальних документів з-під SFC: mergeProcessors дає кілька * блоків script/template з одного `.vue`, graphql-eslint витягує gql з кожного — хибні «дублікати» імені. * Шляхи виглядають як `…/Component.vue/1_script.js/0_document.graphql` (див. debug ESLint `eslint:linter`). */ const graphqlExtractedDocumentLintVueUniqueOperationOverride = { files: ['**/*.vue/**/*.graphql'], ignores: GRAPHQL_EXTRACTED_IGNORES, rules: { '@graphql-eslint/unique-operation-name': 'off' } } /** Processor для .vue у flat config: vue + віртуальні JS-блоки для graphql-eslint (офіційний приклад). */ const vueGraphqlMergedProcessor = mergeProcessors([ vuePlugin.processors.vue, processorVueBlocks({ blocks: { script: true, scriptSetup: true, customBlocks: true } }) ]) /** * Обмежує масив flat-конфігів ESLint заданими `files` (копія кожного елемента). * @param {import('eslint').Linter.Config[]} configs вхідні конфіги * @param {string[]} files glob-патерни файлів * @returns {import('eslint').Linter.Config[]} новий масив flat-конфігів з полем `files` */ function flatConfigsWithFiles(configs, files) { return configs.map(config => ({ ...config, files })) } // Перевизначаємо версію EcmaScript на останню (eslint-plugin-import-x — fork import з flat recommended) const importXPluginEcmaLatest = { ...importXFlatConfigs.recommended, languageOptions: { ...importXFlatConfigs.recommended?.languageOptions, ecmaVersion: 'latest' } } const all = [ { // До дефолтних "**/node_modules/", ".git/" додаємо ignores: ['.yarn/**', '**/dist/**', '**/schema.graphql', '**/adr/**'] }, // Загальні правила для всіх Yaml файлів проекту ...ymlConfigs['flat/recommended'], // JSON / JSONC / JSON5 (рекомендований пресет як у документації плагіна) ...jsoncConfigs['flat/recommended-with-jsonc'], // Лінт коду у fence-блоках .md (процесор). Структуру markdown лінтить markdownlint — // тому беремо лише `processor`, без recommended md-правил (@eslint/markdown; micromark 4.x). ...markdownPlugin.configs.processor, // Плагін eslint-plugin-import-x (правила з префіксом import-x/) importXPluginEcmaLatest, { rules: { 'import-x/no-unresolved': 'off', // не працює з monorepo та #alias 'import-x/newline-after-import': ['error', { count: 1 }], // 'import-x/order': ['error', { warnOnUnassignedImports: true }], 'import-x/no-useless-path-segments': ['error', { noUselessIndex: false }] } }, // Загальні правила для всіх js файлів проекту jsdocPlugin.configs['flat/recommended'], { files: ['**/*.{js,mjs}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module' }, rules: ESLINT_RECOMMENDED_JS_LIKE_RULES }, { files: ['**/*.cjs'], languageOptions: { ecmaVersion: 'latest', sourceType: 'commonjs' }, rules: ESLINT_RECOMMENDED_JS_LIKE_RULES }, // Unicorn правила (лише JS-подібні файли — узгоджено з блоком рекомендованих правил ESLint) ...(Array.isArray(unicornPlugin.configs.recommended) ? unicornPlugin.configs.recommended.map(config => ({ ...config, files: UNICORN_FILES })) : [{ ...unicornPlugin.configs.recommended, files: UNICORN_FILES }]), { files: UNICORN_FILES, rules: { // `continue` у циклах — ок; область збігається з UNICORN_FILES (js/mjs/cjs/vue). 'no-continue': 'off', // Повні дублі e18e-правил (e18e ганяє лише oxlint як jsPlugin; лишаємо e18e-версії, ідентичні за обсягом). 'unicorn/prefer-date-now': 'off', // дубль oxlint `e18e/prefer-date-now` 'unicorn/prefer-regexp-test': 'off', // дубль oxlint `e18e/prefer-regex-test` 'unicorn/filename-case': 'off', 'unicorn/no-null': 'off', 'unicorn/no-useless-concat': 'off', // дубль oxlint/core `no-useless-concat` 'unicorn/prefer-string-slice': 'off', 'unicorn/name-replacements': 'off', 'unicorn/consistent-boolean-name': 'off', 'unicorn/max-nested-calls': 'off', 'unicorn/no-break-in-nested-loop': 'off', 'unicorn/no-computed-property-existence-check': 'off', 'unicorn/no-negated-comparison': 'off', 'unicorn/no-this-outside-of-class': 'off', 'unicorn/no-top-level-assignment-in-function': 'off', 'unicorn/no-top-level-side-effects': 'off', 'unicorn/no-unreadable-for-of-expression': 'off', 'unicorn/prefer-includes-over-repeated-comparisons': 'off', 'unicorn/require-array-sort-compare': 'off', 'unicorn/no-anonymous-default-export': 'off', 'unicorn/prefer-ternary': 'off', 'unicorn/no-array-push-push': 'off', 'unicorn/explicit-length-check': 'off', 'unicorn/prefer-number-properties': 'off', 'unicorn/no-object-as-default-parameter': 'off', 'unicorn/prefer-top-level-await': 'off', 'unicorn/no-nested-ternary': 'off', 'unicorn/prefer-math-min-max': 'off', 'unicorn/numeric-separators-style': [ 'error', { onlyIfContainsSeparator: true, binary: { onlyIfContainsSeparator: false } } ], // unicorn за замовчуванням default для path — вимикаємо, лишаємо лише named (join, resolve тощо) 'unicorn/import-style': [ 'error', { styles: { path: { default: false, namespace: false, named: true }, 'node:path': { default: false, namespace: false, named: true } } } ] } }, // Заборона `for...in` для всіх JS-подібних файлів, включно з Vue SFC (див. `NO_FOR_IN_RESTRICTED_SYNTAX`). { files: UNICORN_FILES, rules: { 'no-restricted-syntax': NO_FOR_IN_RESTRICTED_SYNTAX } }, // eslint-plugin-security (лише JS-подібні файли — як unicorn) { ...securityPlugin.configs.recommended, files: UNICORN_FILES }, { files: UNICORN_FILES, rules: { 'security/detect-non-literal-fs-filename': 'off', 'security/detect-object-injection': 'off', 'security/detect-unsafe-regex': 'off', // `new Buffer(x)` уже покриває `unicorn/no-new-buffer` (з автофіксом на Buffer.from/alloc) // + `@microsoft/sdl/no-unsafe-alloc`; security-версія лише попереджає — вимикаємо як дубль. 'security/detect-new-buffer': 'off' } }, // eslint-plugin-no-unsanitized — innerHTML / insertAdjacentHTML тощо без санітизації (Mozilla; лише JS-подібні файли) { ...noUnsanitized.configs.recommended, files: UNICORN_FILES }, // eslint-plugin-sonarjs — recommended (bugs, complexity, duplicates; лише JS-подібні файли) { ...sonarjsPlugin.configs.recommended, files: UNICORN_FILES }, { files: UNICORN_FILES, rules: { 'sonarjs/no-os-command-from-path': 'off', // Дублі core-ESLint (увімкнені й налаштовані вище, узгоджені з oxlint) — лишаємо core-версії. 'sonarjs/no-fallthrough': 'off', // дубль core `no-fallthrough` 'sonarjs/no-unused-vars': 'off', // дубль core `no-unused-vars` (налаштований під oxlint) 'sonarjs/no-regex-spaces': 'off', // дубль core `no-regex-spaces` 'sonarjs/no-delete-var': 'off', // дубль `no-delete-var` (SDL common ставить, далі делегує oxlint CLI) // Дублі core-правил, які денаїть oxlint CLI (.oxlintrc) — oxlint біжить перед eslint, лишаємо його версії. 'sonarjs/no-control-regex': 'off', // дубль oxlint `no-control-regex` 'sonarjs/no-empty-character-class': 'off', // дубль oxlint `no-empty-character-class` 'sonarjs/no-invalid-regexp': 'off', // дубль oxlint `no-invalid-regexp` 'sonarjs/no-useless-catch': 'off', // дубль oxlint `no-useless-catch` 'sonarjs/no-labels': 'off', // дубль oxlint `no-labels` 'sonarjs/cognitive-complexity': ['warn', 20] } }, /** * Політика репозиторію: коментарі з `TODO` не вважаються порушенням Sonar (на відміну від upstream recommended). */ { files: UNICORN_FILES, rules: { 'sonarjs/todo-tag': 'off' } }, /** * `sonarjs/no-clear-text-protocols` не має `exceptions`, а 0.0.0.0 у `vite.config` типовий для devServer; * вимикаємо правило лише тут (дубль політики покриває `@microsoft/sdl/no-insecure-url` з `SDL_NO_INSECURE_URL_EXCEPTIONS`). */ { files: ['**/vite.config.js', '**/vite.config.mjs', '**/vite.config.ts'], rules: { 'sonarjs/no-clear-text-protocols': 'off' } }, /** * Віртуальні файли з `eslint-processor-vue-blocks` (`…/Component.vue/…_script.js`) без `<template>` — * для них немає `defineTemplateBodyVisitor`, тож `sonarjs/unused-import` хибно скаржиться на імпорти лише для шаблону. */ { files: ['**/*.vue/**/*.js', '**/*.vue/**/*.mjs', '**/*.vue/**/*.cjs', '**/*.vue/**/*.ts', '**/*.vue/**/*.tsx'], rules: { 'sonarjs/unused-import': 'off' } }, // @e18e/eslint-plugin тут НЕ підключаємо: ці правила ганяє лише oxlint як jsPlugin за кореневим // `.oxlintrc.json` (модернізація + module replacements + performance), без дубля в ESLint. // @microsoft/eslint-plugin-sdl — common SDL (без TypeScript/React/Angular; узгоджено з UNICORN_FILES) ...flatConfigsWithFiles(microsoftSdl.configs.common, UNICORN_FILES), { files: UNICORN_FILES, rules: { // Дубль із `eslint-plugin-no-unsanitized` (увімкнено вище): `no-unsanitized/property` покриває // innerHTML/outerHTML, `no-unsanitized/method` — insertAdjacentHTML. До того ж no-unsanitized // розумніший (пропускає безпечні літерали, б'є по недовіреному вводу), тож SDL-дубль вимикаємо. '@microsoft/sdl/no-inner-html': 'off', // Дубль із `no-unsanitized/method` (default-sinks `write`/`writeln` для document.write/writeln). '@microsoft/sdl/no-document-write': 'off', '@microsoft/sdl/no-insecure-url': ['error', { exceptions: SDL_NO_INSECURE_URL_EXCEPTIONS }] } } ] // Тільки для node js проектів /* Друга частина SDL node: лише правила microsoft-sdl; перший блок upstream дублює реєстрацію eslint-plugin-n. */ const microsoftSdlNodeConfigsWithoutN = microsoftSdl.configs.node.slice(1) /** * Додаткові правила та глобалі Bun поверх пресетів eslint-plugin-n для кожного шару під `params.node`. */ const NODE_ESLINT_PLUGIN_N_EXTRA = { rules: { 'n/no-missing-import': 'off', // покривається oxlint https://github.com/oxc-project/oxc/issues/481#issuecomment-3135766557 'n/no-deprecated-api': 'error' // як перший блок `configs.node` у @microsoft/eslint-plugin-sdl }, languageOptions: { globals: { ...globals.node, Bun: 'readonly' } } } /** Три шари: `.js` (рекомендований пресет за `type` у package.json), `.mjs` (ESM), `.cjs` (CommonJS). */ const NODE_PLUGIN_N_MIXED = nodePlugin.configs['flat/mixed-esm-and-cjs'] /** * Flat-конфіги eslint-plugin-n для заданих кореневих директорій Node-коду (узгоджено з `flat/mixed-esm-and-cjs`). * @param {string[]} dirNames елементи `params.node` * @returns {import('eslint').Linter.Config[]} шість flat-конфігів (три від пресету плагіна n × два шари з NODE_ESLINT_PLUGIN_N_EXTRA) */ function flatConfigsNodePluginNForDirectories(dirNames) { const filesByKind = [ dirNames.map(name => `${name}/**/*.js`), dirNames.map(name => `${name}/**/*.mjs`), dirNames.map(name => `${name}/**/*.cjs`) ] /** @type {import('eslint').Linter.Config[]} */ const out = [] for (let i = 0; i < 3; i++) { out.push( { ...NODE_PLUGIN_N_MIXED[i], files: filesByKind[i] }, { ...NODE_ESLINT_PLUGIN_N_EXTRA, files: filesByKind[i] } ) } return out } // Тільки для Vue проектів // files: ['**/vite.config.js'] const vueVite = [ { languageOptions: { globals: { process: 'readonly' } } } ] /** * Глобальні змінні для типового Vite + auto-import: Composition API, vue-router, Quasar, пакет vue-apollo-composable. * У пакеті `globals` ключ `vue` містить лише макроси `<script setup>` (`defineProps` тощо), без `onMounted` / * `createApp` / `useRouter` — без цього списку `no-undef` хибно спрацьовує у `.vue` і `.js` під директоріями Vue. */ const VUE_VITE_AUTO_IMPORT_GLOBALS = Object.fromEntries( [ 'cloneVNode', 'computed', 'createApp', 'customRef', 'defineAsyncComponent', 'defineComponent', 'effectScope', 'getCurrentInstance', 'getCurrentScope', 'h', 'inject', 'isProxy', 'isReactive', 'isReadonly', 'isRef', 'isVNode', 'markRaw', 'mergeProps', 'nextTick', 'onActivated', 'onBeforeMount', 'onBeforeRouteLeave', 'onBeforeRouteUpdate', 'onBeforeUnmount', 'onBeforeUpdate', 'onDeactivated', 'onErrorCaptured', 'onMounted', 'onRenderTracked', 'onRenderTriggered', 'onScopeDispose', 'onServerPrefetch', 'onUnmounted', 'onUpdated', 'provide', 'reactive', 'readonly', 'ref', 'resolveComponent', 'resolveDirective', 'shallowReactive', 'shallowReadonly', 'shallowRef', 'toRef', 'toRefs', 'toValue', 'triggerRef', 'unref', 'useApolloClient', 'useAttrs', 'useCssModule', 'useCssVars', 'useId', 'useLazyQuery', 'useLink', 'useModel', 'useMutation', 'useQuasar', 'useQuery', 'useRoute', 'useRouter', 'useSlots', 'useSubscription', 'useTemplateRef', 'watch', 'watchEffect', 'watchPostEffect', 'watchSyncEffect', 'withDirectives', 'withModifiers' ].map(name => [name, 'readonly']) ) // Для усіх файлів Vue проектів // files: ['**/*.js', '**/*.vue'] const vueAllVite = [ { languageOptions: { globals: { ...globals.browser, Intl: 'readonly', createLogger: 'readonly', gql: 'readonly', $ref: 'readonly', $computed: 'readonly', ...VUE_VITE_AUTO_IMPORT_GLOBALS, // Макроси `<script setup>` (у пакеті `globals` — лише `globals.vue`). ...globals.vue } } } ] const vue = [ ...vuePlugin.configs['flat/recommended'], { rules: { 'vue/no-v-html': 'off', 'vue/html-indent': 'off', 'vue/max-attributes-per-line': 'off', 'vue/html-closing-bracket-newline': 'off', 'vue/singleline-html-element-content-newline': 'off', 'vue/multi-word-component-names': 'off', 'vue/html-self-closing': [ 'error', { html: { void: 'always' // узгоджено з oxfmt / стабільним форматуванням void-елементів } } ], // Нітра порядок сортування 'vue/attributes-order': [ 'error', { order: [ 'DEFINITION', 'LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', ['UNIQUE', 'SLOT'], 'TWO_WAY_BINDING', 'OTHER_DIRECTIVES', 'EVENTS', 'OTHER_ATTR', 'CONTENT' ], alphabetical: false } ], // Nitra порядок блоків SFC: template → script → style (замість recommended script → template → style) 'vue/block-order': ['warn', { order: ['template', 'script', 'style'] }] } } ] const vue2 = [ { rules: { 'vue/no-deprecated-v-bind-sync': 'off' } } ] /** * Увімкнути graphql-eslint processor лише для .js (gql у коді). * @param {import('eslint').Linter.Config[]} result масив конфігів * @param {string[]} dirs кореневі директорії */ function pushGraphqlJsProcessors(result, dirs) { if (!dirs?.length) { return } result.push({ files: dirs.map(name => `${name}/**/*.js`), processor: graphqlCodefileProcessor }) } /** * Merge processor для .vue (vue + vue-blocks + graphql) — має бути останнім серед конфігів для `*.vue`, * інакше пресет eslint-plugin-vue (`processor: "vue/vue"`) перезапише його. * @param {import('eslint').Linter.Config[]} result масив конфігів * @param {string[]} dirs кореневі директорії Vue-пакетів */ function pushGraphqlVueMergedProcessorLast(result, dirs) { if (!dirs?.length) { return } result.push({ files: dirs.map(name => `${name}/**/*.vue`), processor: vueGraphqlMergedProcessor }) } /** * Після oxlint додає для `.vue` правила з `eslint:recommended`, які не делегує `eslint-plugin-oxlint` і які * не ввімкнені в oxlint за `.oxlintrc.json` (див. `VUE_SCRIPT_ESLINT_RECOMMENDED_GAP_RULES`). * @param {import('eslint').Linter.Config[]} result масив конфігів * @param {string[]} dirs кореневі директорії Vue-пакетів (ті самі аргументи dirs, що й у pushVueConfigs) */ function pushVueScriptEslintRecommendedGapsAfterOxlint(result, dirs) { if (!dirs?.length) { return } result.push({ name: 'nitra/vue-script-eslint-recommended-oxlint-gaps', files: dirs.map(name => `${name}/**/*.vue`), // Правила з `eslint:recommended` + `no-unused-vars` з опціями; тип `RuleConfig` у ESLint 10 вимагає кортеж, тож подвійне приведення. rules: /** @type {import('eslint').Linter.RulesRecord} */ ( /** @type {unknown} */ (VUE_SCRIPT_ESLINT_RECOMMENDED_GAP_RULES) ) }) } /** * Додає до result конфіги для Vue-проєктів (vite, .vue, .js). * @param {import('eslint').Linter.Config[]} result масив конфігів * @param {string[]} dirs директорії Vue-проєктів * @param {{ includeVite?: boolean, extraVueConfigs?: import('eslint').Linter.Config[] }} [options] опції: includeVite, extraVueConfigs */ function pushVueConfigs(result, dirs, options = {}) { const { includeVite = false, extraVueConfigs = [] } = options if (includeVite) { const filesVite = dirs.map(name => `${name}/**/vite.config.js`) result.push(...vueVite.map(configObject => ({ files: filesVite, ...configObject }))) } const filesVue = dirs.map(name => `${name}/**/*.vue`) const vueConfigs = [...vueAllVite, ...vue, ...extraVueConfigs] result.push(...vueConfigs.map(configObject => ({ files: filesVue, ...configObject })), { files: filesVue, languageOptions: { parser: vueEslintParser } }) const filesJS = dirs.map(name => `${name}/**/*.js`) result.push(...vueAllVite.map(configObject => ({ files: filesJS, ...configObject }))) } /** * Нормалізує oxlint-важкість у ту, яку розуміє `eslint-plugin-oxlint`: нативні `deny`/`allow` він не вважає * «активними» (лише `error`/`warn`/`1`/`2`), тож без `deny`→`error` він не вимкнув би жодного дубля. * @param {unknown} value важкість правила з `.oxlintrc.json` (рядок або елемент кортежу) * @returns {unknown} `error` для `deny`, `off` для `allow`, інакше без змін */ function normalizeOxlintSeverity(value) { if (value === 'deny') { return 'error' } if (value === 'allow') { return 'off' } return value } /** * Вимикає в ESLint правила, які вже виконує oxlint за кореневим `.oxlintrc.json` споживача, щоб уникнути * подвійних порушень і конкурентних `--fix` за один намір (політика `oxlint-eslint-config.mdc`). * * Поза нормалізацією важкості (див. `normalizeOxlintSeverity`) є одна поправка namespace: oxlint-плагін * зветься `import` і `eslint-plugin-oxlint` мапить його в ESLint-префікс `import` (eslint-plugin-import), * але тут активний `eslint-plugin-import-x` (`import-x/`). Тож кожен off для `import/*` дублюємо як `import-x/*`. * * `@e18e/*` сюди не входить (це `jsPlugins` oxlint, плагін про нього не знає) — для e18e дублів у ESLint нема. * Якщо кореневого `.oxlintrc.json` немає — повертає `[]` (дублів за ним теж немає). * @returns {import('eslint').Linter.Config[]} flat-конфіги виду `{ rules: { …: 'off' } }` */ function oxlintEnabledRulesOff() { let oxlintConfig try { oxlintConfig = JSON.parse(readFileSync('.oxlintrc.json', 'utf8')) } catch { return [] } const normalizedRules = Object.fromEntries( Object.entries(oxlintConfig.rules ?? {}).map(([name, value]) => [ name, Array.isArray(value) ? [normalizeOxlintSeverity(value[0]), ...value.slice(1)] : normalizeOxlintSeverity(value) ]) ) // Лишаємо тільки rule-конфіги (відкидаємо згенерований ignorePatterns-блок: ігнори синхронізуємо окремо). const configs = oxlint.buildFromOxlintConfig({ ...oxlintConfig, rules: normalizedRules }).filter(c => c.rules) for (const config of configs) { for (const ruleName of Object.keys(config.rules)) { if (ruleName.startsWith('import/')) { config.rules[`import-x/${ruleName.slice('import/'.length)}`] = 'off' } } } return configs } /** * ESLint flat config для проєктів на Vue та Node. * @param {object} [params] — список директорій для застосування правил * @param {string[]} [params.node] — шляхи до Node.js коду; правила та `globals.node` застосовуються до файлів * `.js`, `.mjs` та `.cjs` у дереві кожної директорії (eslint-plugin-n `flat/mixed-esm-and-cjs`: для `.cjs` — * script/CommonJS, для `.mjs` — module, для `.js` — за полем `type` у package.json проєкту). * @param {string[]} [params.vue] — шляхи до Vue 3 * @param {string[]} [params.vue2] — шляхи до Vue 2 * @returns {import('eslint').Linter.Config[]} масив конфігурацій ESLint */ // oxlint-disable-next-line unicorn/no-object-as-default-parameter export function getConfig(params = { node: [], vue: [], vue2: [] }) { const result = [...all] let graphqlCodefile = false /** Директорії Vue/Vue2, для яких потрібен merge processor після oxlint. */ const graphqlVueDirsLast = [] if (params.node?.length) { const files = params.node.flatMap(name => [`${name}/**/*.js`, `${name}/**/*.mjs`, `${name}/**/*.cjs`]) result.push( ...flatConfigsNodePluginNForDirectories(params.node), ...flatConfigsWithFiles(microsoftSdlNodeConfigsWithoutN, files) ) // Каталог `npm` пакета конфігу зазвичай без graphql-config; перший preprocess інакше кешує null і ламає демо/vue. const graphqlNodeDirs = params.node.filter(d => d !== 'npm') if (graphqlNodeDirs.length) { pushGraphqlJsProcessors(result, graphqlNodeDirs) graphqlCodefile = true } } if (params.vue?.length) { pushVueConfigs(result, params.vue, { includeVite: true }) pushGraphqlJsProcessors(result, params.vue) graphqlVueDirsLast.push(...params.vue) graphqlCodefile = true } if (params.vue2?.length) { pushVueConfigs(result, params.vue2, { extraVueConfigs: vue2 }) pushGraphqlJsProcessors(result, params.vue2) graphqlVueDirsLast.push(...params.vue2) graphqlCodefile = true } if (graphqlCodefile) { result.push(graphqlExtractedDocumentLint, graphqlExtractedDocumentLintVueUniqueOperationOverride) } // 1) `flat/recommended` — вимикає в ESLint клас correctness, який oxlint виконує за замовчуванням. // 2) `oxlintEnabledRulesOff()` — додатково вимикає правила, увімкнені (`deny`) у кореневому `.oxlintrc.json` // поза correctness: unicorn pedantic/style, jsdoc, core (`eqeqeq`, `no-else-return`…), `node/*`→`n/*`, // `import/*`→`import-x/*`, e18e тощо — щоб oxlint і ESLint не дублювали ці перевірки. // Має йти ПЕРЕД vue-gap блоком: серед off-правил є `no-unused-vars`, який далі знову вмикається для `.vue`. result.push(...oxlint.configs['flat/recommended'], ...oxlintEnabledRulesOff()) if (graphqlVueDirsLast.length) { pushGraphqlVueMergedProcessorLast(result, graphqlVueDirsLast) pushVueScriptEslintRecommendedGapsAfterOxlint(result, graphqlVueDirsLast) } return result }