@nitra/eslint-config
Version:
An ESLint shareable config for projects using Vue and Node
822 lines (778 loc) • 35.6 kB
JavaScript
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
}