@nitra/eslint-config 3.4.3 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,7 +79,7 @@ export default [
79
79
 
80
80
  ## Що входить у конфіг
81
81
 
82
- - Базові правила JS (recommended), JSDoc, Import, Unicorn
82
+ - Базові правила JS (recommended), JSDoc, **eslint-plugin-import-x** (префікс `import-x/`), Unicorn
83
83
  - YAML і Markdown
84
84
  - Для **Node**: eslint-plugin-n
85
85
  - Для **Vue**: eslint-plugin-vue з правилами атрибутів та порядку
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Обхід обмеження graphql-eslint: loadOnDiskGraphQLConfig(filePath) для віртуальних
3
+ * шляхів виду …/Component.vue/…/script.js має отримувати шлях до фізичного .vue, інакше rootDir
4
+ * стає некоректним і eslint не бачить схему з .graphqlrc.yml.
5
+ * @see upstream graphql-eslint (MIT) on GitHub
6
+ */
7
+ import { relative } from 'node:path'
8
+ import { gqlPluckFromCodeStringSync } from '@graphql-tools/graphql-tag-pluck'
9
+ import { asArray } from '@graphql-tools/utils'
10
+ import graphqlEslintPlugin from '@graphql-eslint/eslint-plugin'
11
+ import { loadOnDiskGraphQLConfig } from '@graphql-eslint/eslint-plugin/graphql-config'
12
+ import { version } from '@graphql-eslint/eslint-plugin/meta'
13
+ import { CWD, REPORT_ON_FIRST_CHARACTER } from '@graphql-eslint/eslint-plugin/utils'
14
+
15
+ const blocksMap = /* @__PURE__ */ new Map()
16
+ let onDiskConfig
17
+ let onDiskConfigLoaded = false
18
+ const RELEVANT_KEYWORDS = ['gql', 'graphql', 'GraphQL']
19
+
20
+ /** Регулярний вираз на рівні модуля для e18e/prefer-static-regex. */
21
+ const VUE_OR_SVELTE_FILE_RE = /\.(vue|svelte)$/
22
+
23
+ /**
24
+ * Шлях для graphql-config: для віртуальних блоків Vue — фізичний .vue, інакше той самий filePath.
25
+ * @param {string} filePath шлях файлу, який обробляє processor
26
+ * @returns {string} шлях для loadOnDiskGraphQLConfig та getProjectForFile
27
+ */
28
+ export function graphQLConfigPathAnchor(filePath = '') {
29
+ if (filePath.includes('.vue/')) {
30
+ return filePath.slice(0, filePath.indexOf('.vue/') + '.vue'.length)
31
+ }
32
+ return filePath
33
+ }
34
+
35
+ /** Processor як у graphql-eslint, зі стабільним anchor для graphql-config. */
36
+ export const graphqlCodefileProcessor = {
37
+ meta: {
38
+ name: '@nitra/graphql-codefile-processor',
39
+ version
40
+ },
41
+ supportsAutofix: true,
42
+ preprocess(code, filePath) {
43
+ if (process.env.ESLINT_USE_FLAT_CONFIG !== 'false' && filePath.endsWith('.vue')) {
44
+ throw new Error(
45
+ 'Processing of `.vue` files is no longer supported, follow the new official vue example for ESLint flat config https://github.com/graphql-hive/graphql-eslint/tree/master/examples/vue-code-file'
46
+ )
47
+ }
48
+ if (!onDiskConfigLoaded) {
49
+ onDiskConfig = loadOnDiskGraphQLConfig(graphQLConfigPathAnchor(filePath))
50
+ onDiskConfigLoaded = true
51
+ }
52
+ let keywords = RELEVANT_KEYWORDS
53
+ const anchor = graphQLConfigPathAnchor(filePath)
54
+ const pluckConfig = onDiskConfig?.getProjectForFile(anchor)?.extensions?.pluckConfig
55
+ if (pluckConfig) {
56
+ const { modules = [], globalGqlIdentifierName = ['gql', 'graphql'], gqlMagicComment = 'GraphQL' } = pluckConfig
57
+ const mods = modules.map(({ identifier }) => identifier).filter(v => !!v)
58
+ const result = [...mods, ...asArray(globalGqlIdentifierName), gqlMagicComment]
59
+ keywords = [...new Set(result)]
60
+ }
61
+ if (keywords.every(keyword => !code.includes(keyword))) {
62
+ return [code]
63
+ }
64
+ try {
65
+ const sources = gqlPluckFromCodeStringSync(filePath, code, {
66
+ skipIndent: true,
67
+ ...pluckConfig
68
+ })
69
+ const blocks = sources.map(item => ({
70
+ filename: 'document.graphql',
71
+ text: item.body,
72
+ lineOffset: item.locationOffset.line - 1,
73
+ offset: item.locationOffset.index + 1
74
+ }))
75
+ blocksMap.set(filePath, blocks)
76
+ return [...blocks, code]
77
+ } catch (error) {
78
+ if (error instanceof Error) {
79
+ error.message = `[graphql-eslint] Error while preprocessing "${relative(CWD, filePath)}" file
80
+
81
+ ${error.message}`
82
+ }
83
+ console.error(error)
84
+ return [code]
85
+ }
86
+ },
87
+ postprocess(messages, filePath) {
88
+ const blocks = blocksMap.get(filePath) || []
89
+ for (const [i, block] of blocks.entries()) {
90
+ const { lineOffset, offset } = block
91
+ for (const message of messages[i] || []) {
92
+ const isVueOrSvelte = VUE_OR_SVELTE_FILE_RE.test(filePath)
93
+ if (isVueOrSvelte) {
94
+ delete message.endLine
95
+ delete message.endColumn
96
+ delete message.fix
97
+ delete message.suggestions
98
+ Object.assign(message, REPORT_ON_FIRST_CHARACTER)
99
+ continue
100
+ }
101
+ message.line += lineOffset
102
+ if (typeof message.endLine === 'number') {
103
+ message.endLine += lineOffset
104
+ }
105
+ if (message.fix) {
106
+ message.fix.range[0] += offset
107
+ message.fix.range[1] += offset
108
+ }
109
+ for (const suggestion of message.suggestions || []) {
110
+ const [start, end] = suggestion.fix.range
111
+ suggestion.fix.range = [start + offset, end + offset]
112
+ }
113
+ }
114
+ }
115
+ const result = messages.flat()
116
+ return result.toSorted((a, b) => a.line - b.line || a.column - b.column)
117
+ }
118
+ }
119
+
120
+ const baseParser = graphqlEslintPlugin.parser
121
+
122
+ /**
123
+ * Парсер graphql-eslint: той самий anchor для filePath, щоб loadGraphQLConfig знаходив проєкт для віртуальних document.graphql з-під Vue.
124
+ * @param {string} code вміст GraphQL-документа
125
+ * @param {import('eslint').Linter.ParserOptions} options опції ESLint (filePath тощо)
126
+ * @returns {ReturnType<typeof baseParser.parseForESLint>} AST і parserServices для graphql-eslint
127
+ */
128
+ export function graphqlParseForESLintWithAnchor(code, options = {}) {
129
+ const filePath = options.filePath || ''
130
+ return baseParser.parseForESLint(code, {
131
+ ...options,
132
+ filePath: graphQLConfigPathAnchor(filePath)
133
+ })
134
+ }
135
+
136
+ /** Парсер для flat config (обгортка над graphql-eslint). */
137
+ export const graphqlParserWithConfigAnchor = {
138
+ meta: baseParser.meta,
139
+ parseForESLint: graphqlParseForESLintWithAnchor
140
+ }
package/index.js CHANGED
@@ -1,6 +1,10 @@
1
+ import e18ePlugin from '@e18e/eslint-plugin'
2
+ import { fixupPluginRules } from '@eslint/compat'
1
3
  import js from '@eslint/js'
4
+ import graphqlEslintPlugin from '@graphql-eslint/eslint-plugin'
5
+ import { graphqlCodefileProcessor, graphqlParserWithConfigAnchor } from './graphql-eslint-anchor.js'
2
6
  import microsoftSdl from '@microsoft/eslint-plugin-sdl'
3
- import { flatConfigs as importPlugin } from 'eslint-plugin-import'
7
+ import { flatConfigs as importXFlatConfigs } from 'eslint-plugin-import-x'
4
8
  import jsdocPlugin from 'eslint-plugin-jsdoc'
5
9
  import { configs as jsoncConfigs } from 'eslint-plugin-jsonc'
6
10
  import markdownPlugin from 'eslint-plugin-markdown'
@@ -10,11 +14,62 @@ import securityPlugin from 'eslint-plugin-security'
10
14
  import unicornPlugin from 'eslint-plugin-unicorn'
11
15
  import vuePlugin from 'eslint-plugin-vue'
12
16
  import { configs as ymlConfigs } from 'eslint-plugin-yml'
17
+ import { mergeProcessors } from 'eslint-merge-processors'
18
+ import processorVueBlocks from 'eslint-processor-vue-blocks'
13
19
  import globals from 'globals'
14
20
 
15
21
  /** Glob-патерни файлів для eslint-plugin-unicorn (JS-подібні джерела; без сирих YAML/Markdown). */
16
22
  const UNICORN_FILES = ['**/*.{js,mjs,cjs,vue}']
17
23
 
24
+ /** Пресет recommended `@e18e/eslint-plugin` (flat). */
25
+ // @ts-expect-error типізація плагіна не гарантує `configs.recommended`; у runtime поле є.
26
+ const e18eRecommendedFlat = /** @type {import('eslint').Linter.FlatConfig} */ (e18ePlugin.configs.recommended)
27
+
28
+ /** Віртуальні документи з gql/graphql у .js/.vue (processor); ESLint обробляє їх як окремі `*.graphql` файли. */
29
+ const GRAPHQL_EXTRACTED_DOCUMENT_FILES = ['**/*.graphql']
30
+
31
+ /**
32
+ * Правила для витягнутих GraphQL-блоків: пресет operations-recommended (поля, типи, змінні).
33
+ * Схема та documents підтягуються через graphql-config (файл налаштувань у корені пакета з операціями, зазвичай YAML).
34
+ */
35
+ const graphqlExtractedDocumentLint = {
36
+ files: GRAPHQL_EXTRACTED_DOCUMENT_FILES,
37
+ languageOptions: {
38
+ parser: graphqlParserWithConfigAnchor
39
+ },
40
+ plugins: {
41
+ // ESLint 10: graphql-eslint ще викликає context.getSourceCode() у частині правил — fixup з @eslint/compat.
42
+ '@graphql-eslint': fixupPluginRules(graphqlEslintPlugin)
43
+ },
44
+ rules: {
45
+ ...graphqlEslintPlugin.configs['flat/operations-recommended'].rules
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Вимкнення unique-operation-name лише для віртуальних документів з-під SFC: mergeProcessors дає кілька
51
+ * блоків script/template з одного `.vue`, graphql-eslint витягує gql з кожного — хибні «дублікати» імені.
52
+ * Шляхи виглядають як `…/Component.vue/1_script.js/0_document.graphql` (див. debug ESLint `eslint:linter`).
53
+ */
54
+ const graphqlExtractedDocumentLintVueUniqueOperationOverride = {
55
+ files: ['**/*.vue/**/*.graphql'],
56
+ rules: {
57
+ '@graphql-eslint/unique-operation-name': 'off'
58
+ }
59
+ }
60
+
61
+ /** Processor для .vue у flat config: vue + віртуальні JS-блоки для graphql-eslint (офіційний приклад). */
62
+ const vueGraphqlMergedProcessor = mergeProcessors([
63
+ vuePlugin.processors.vue,
64
+ processorVueBlocks({
65
+ blocks: {
66
+ script: true,
67
+ scriptSetup: true,
68
+ customBlocks: true
69
+ }
70
+ })
71
+ ])
72
+
18
73
  /**
19
74
  * Обмежує масив flat-конфігів ESLint заданими `files` (копія кожного елемента).
20
75
  * @param {import('eslint').Linter.FlatConfig[]} configs вхідні конфіги
@@ -25,11 +80,11 @@ function flatConfigsWithFiles(configs, files) {
25
80
  return configs.map(config => ({ ...config, files }))
26
81
  }
27
82
 
28
- // Перевизначаємо версію EcmaScript на останню
29
- const importPluginEcmaLatest = {
30
- ...importPlugin.recommended,
83
+ // Перевизначаємо версію EcmaScript на останню (eslint-plugin-import-x — fork import з flat recommended)
84
+ const importXPluginEcmaLatest = {
85
+ ...importXFlatConfigs.recommended,
31
86
  languageOptions: {
32
- ...importPlugin.recommended?.languageOptions,
87
+ ...importXFlatConfigs.recommended?.languageOptions,
33
88
  ecmaVersion: 'latest'
34
89
  }
35
90
  }
@@ -37,7 +92,7 @@ const importPluginEcmaLatest = {
37
92
  const all = [
38
93
  {
39
94
  // До дефолтних "**/node_modules/", ".git/" додаємо
40
- ignores: ['.yarn/**', '**/dist/**']
95
+ ignores: ['.yarn/**', '**/dist/**', '**/schema.graphql']
41
96
  },
42
97
  // Загальні правила для всіх Yaml файлів проекту
43
98
  ...ymlConfigs['flat/recommended'],
@@ -45,14 +100,14 @@ const all = [
45
100
  ...jsoncConfigs['flat/recommended-with-jsonc'],
46
101
  // Загальні правила для всіх MD файлів проекту
47
102
  ...markdownPlugin.configs.recommended,
48
- // Плагін eslint-plugin-import
49
- importPluginEcmaLatest,
103
+ // Плагін eslint-plugin-import-x (правила з префіксом import-x/)
104
+ importXPluginEcmaLatest,
50
105
  {
51
106
  rules: {
52
- 'import/no-unresolved': 'off', // не працює з monorepo та #alias
53
- 'import/newline-after-import': ['error', { count: 1 }],
54
- // 'import/order': ['error', { warnOnUnassignedImports: true }],
55
- 'import/no-useless-path-segments': ['error', { noUselessIndex: false }]
107
+ 'import-x/no-unresolved': 'off', // не працює з monorepo та #alias
108
+ 'import-x/newline-after-import': ['error', { count: 1 }],
109
+ // 'import-x/order': ['error', { warnOnUnassignedImports: true }],
110
+ 'import-x/no-useless-path-segments': ['error', { noUselessIndex: false }]
56
111
  }
57
112
  },
58
113
  // Загальні правила для всіх js файлів проекту
@@ -114,6 +169,11 @@ const all = [
114
169
  'security/detect-unsafe-regex': 'off'
115
170
  }
116
171
  },
172
+ // @e18e/eslint-plugin — recommended (modernization + module replacements + performance)
173
+ {
174
+ files: UNICORN_FILES,
175
+ ...e18eRecommendedFlat
176
+ },
117
177
  // @microsoft/eslint-plugin-sdl — common SDL (без TypeScript/React/Angular; узгоджено з UNICORN_FILES)
118
178
  ...flatConfigsWithFiles(microsoftSdl.configs.common, UNICORN_FILES)
119
179
  ]
@@ -219,6 +279,37 @@ const vue2 = [
219
279
  }
220
280
  ]
221
281
 
282
+ /**
283
+ * Увімкнути graphql-eslint processor лише для .js (gql у коді).
284
+ * @param {import('eslint').Linter.FlatConfig[]} result масив конфігів
285
+ * @param {string[]} dirs кореневі директорії
286
+ */
287
+ function pushGraphqlJsProcessors(result, dirs) {
288
+ if (!dirs?.length) {
289
+ return
290
+ }
291
+ result.push({
292
+ files: dirs.map(name => `${name}/**/*.js`),
293
+ processor: graphqlCodefileProcessor
294
+ })
295
+ }
296
+
297
+ /**
298
+ * Merge processor для .vue (vue + vue-blocks + graphql) — має бути останнім серед конфігів для `*.vue`,
299
+ * інакше пресет eslint-plugin-vue (`processor: "vue/vue"`) перезапише його.
300
+ * @param {import('eslint').Linter.FlatConfig[]} result масив конфігів
301
+ * @param {string[]} dirs кореневі директорії Vue-пакетів
302
+ */
303
+ function pushGraphqlVueMergedProcessorLast(result, dirs) {
304
+ if (!dirs?.length) {
305
+ return
306
+ }
307
+ result.push({
308
+ files: dirs.map(name => `${name}/**/*.vue`),
309
+ processor: vueGraphqlMergedProcessor
310
+ })
311
+ }
312
+
222
313
  /**
223
314
  * Додає до result конфіги для Vue-проєктів (vite, .vue, .js).
224
315
  * @param {import('eslint').Linter.FlatConfig[]} result масив конфігів
@@ -252,6 +343,9 @@ function pushVueConfigs(result, dirs, options = {}) {
252
343
  // oxlint-disable-next-line unicorn/no-object-as-default-parameter
253
344
  export function getConfig(params = { node: [], vue: [], vue2: [] }) {
254
345
  const result = [...all]
346
+ let graphqlCodefile = false
347
+ /** Директорії Vue/Vue2, для яких потрібен merge processor після oxlint. */
348
+ const graphqlVueDirsLast = []
255
349
 
256
350
  if (params.node?.length) {
257
351
  const files = params.node.map(name => `${name}/**/*.js`)
@@ -259,16 +353,37 @@ export function getConfig(params = { node: [], vue: [], vue2: [] }) {
259
353
  ...node.map(configObject => ({ files, ...configObject })),
260
354
  ...flatConfigsWithFiles(microsoftSdlNodeConfigsWithoutN, files)
261
355
  )
356
+ // Каталог `npm` пакета конфігу зазвичай без graphql-config; перший preprocess інакше кешує null і ламає демо/vue.
357
+ const graphqlNodeDirs = params.node.filter(d => d !== 'npm')
358
+ if (graphqlNodeDirs.length) {
359
+ pushGraphqlJsProcessors(result, graphqlNodeDirs)
360
+ graphqlCodefile = true
361
+ }
262
362
  }
263
363
 
264
364
  if (params.vue?.length) {
265
365
  pushVueConfigs(result, params.vue, { includeVite: true })
366
+ pushGraphqlJsProcessors(result, params.vue)
367
+ graphqlVueDirsLast.push(...params.vue)
368
+ graphqlCodefile = true
266
369
  }
267
370
 
268
371
  if (params.vue2?.length) {
269
372
  pushVueConfigs(result, params.vue2, { extraVueConfigs: vue2 })
373
+ pushGraphqlJsProcessors(result, params.vue2)
374
+ graphqlVueDirsLast.push(...params.vue2)
375
+ graphqlCodefile = true
376
+ }
377
+
378
+ if (graphqlCodefile) {
379
+ result.push(graphqlExtractedDocumentLint, graphqlExtractedDocumentLintVueUniqueOperationOverride)
270
380
  }
271
381
 
272
382
  result.push(...oxlint.configs['flat/recommended'])
383
+
384
+ if (graphqlVueDirsLast.length) {
385
+ pushGraphqlVueMergedProcessorLast(result, graphqlVueDirsLast)
386
+ }
387
+
273
388
  return result
274
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/eslint-config",
3
- "version": "3.4.3",
3
+ "version": "3.5.0",
4
4
  "description": "An ESLint shareable config for projects using Vue and Node",
5
5
  "keywords": [
6
6
  "eslint",
@@ -14,6 +14,7 @@
14
14
  "url": "git@github.com:nitra/eslint-config.git"
15
15
  },
16
16
  "files": [
17
+ "graphql-eslint-anchor.js",
17
18
  "index.d.ts",
18
19
  "index.js"
19
20
  ],
@@ -21,20 +22,28 @@
21
22
  "types": "./index.d.ts",
22
23
  "exports": "./index.js",
23
24
  "dependencies": {
25
+ "@e18e/eslint-plugin": "^0.3.0",
26
+ "@eslint/compat": "^2.0.5",
24
27
  "@eslint/js": "^10.0.1",
28
+ "@graphql-eslint/eslint-plugin": "^4.4.0",
29
+ "@graphql-tools/graphql-tag-pluck": "^8.3.4",
30
+ "@graphql-tools/utils": "^10.0.0",
25
31
  "@microsoft/eslint-plugin-sdl": "^1.1.0",
26
32
  "eslint": "^10.2.0",
27
- "eslint-plugin-import": "^2.32.0",
33
+ "eslint-merge-processors": "^2.0.0",
34
+ "eslint-plugin-import-x": "^4.16.2",
28
35
  "eslint-plugin-jsdoc": "^62.9.0",
29
36
  "eslint-plugin-jsonc": "^3.1.2",
30
37
  "eslint-plugin-markdown": "^5.1.0",
31
38
  "eslint-plugin-n": "^17.24.0",
32
- "eslint-plugin-oxlint": "^1.58.0",
39
+ "eslint-plugin-oxlint": "^1.59.0",
33
40
  "eslint-plugin-security": "^4.0.0",
34
41
  "eslint-plugin-unicorn": "^64.0.0",
35
42
  "eslint-plugin-vue": "^10.8.0",
36
43
  "eslint-plugin-yml": "^3.3.1",
37
- "globals": "^17.4.0",
44
+ "eslint-processor-vue-blocks": "^2.0.0",
45
+ "globals": "^17.5.0",
46
+ "graphql": "^16.11.0",
38
47
  "vue-eslint-parser": "^10.4.0"
39
48
  },
40
49
  "engines": {