UNPKG

@nitra/eslint-config

Version:

An ESLint shareable config for projects using Vue and Node

214 lines (200 loc) 9.61 kB
/** * Обхід обмеження graphql-eslint: loadOnDiskGraphQLConfig(filePath) для віртуальних * шляхів виду …/Component.vue/…/script.js має отримувати шлях до фізичного .vue, інакше rootDir * стає некоректним і eslint не бачить схему з .graphqlrc.yml. * @see upstream graphql-eslint (MIT) on GitHub */ import { relative } from 'node:path' import { env } from 'node:process' import { CodeFileLoader } from '@graphql-tools/code-file-loader' import { gqlPluckFromCodeStringSync } from '@graphql-tools/graphql-tag-pluck' import { asArray } from '@graphql-tools/utils' import graphqlEslintPlugin from '@graphql-eslint/eslint-plugin' import { loadOnDiskGraphQLConfig } from '@graphql-eslint/eslint-plugin/graphql-config' import { version } from '@graphql-eslint/eslint-plugin/meta' import { CWD, REPORT_ON_FIRST_CHARACTER } from '@graphql-eslint/eslint-plugin/utils' /** * Дефолтні негативні globs для CodeFileLoader: апстрім `createGlobbyOptions` хардкодить * `ignore: []`, тож широкі patterns у `.graphqlrc.yml documents` (наприклад, * `**\/*.{vue,js,ts,tsx}`) сканують `node_modules` синхронно через Babel pluck і вішають * eslint на хвилини. Patches `_buildGlobs` додає `!`-префіксні globs у сам список патернів — * це працює, на відміну від спроби пропатчити free function `createGlobbyOptions`. */ const CODE_FILE_LOADER_DEFAULT_IGNORES = [ '!**/node_modules/**', '!**/.git/**', '!**/dist/**', '!**/.yarn/**', '!**/.cursor/**', '!**/.claude/**', '!**/.github/**', '!**/.vscode/**', '!**/coverage/**' ] const CODE_FILE_LOADER_PATCH_FLAG = Symbol.for('@nitra/eslint-config/code-file-loader-default-ignores') if (!CodeFileLoader.prototype[CODE_FILE_LOADER_PATCH_FLAG]) { const originalBuildGlobs = CodeFileLoader.prototype._buildGlobs CodeFileLoader.prototype._buildGlobs = function patchedBuildGlobs(glob, options) { const globs = originalBuildGlobs.call(this, glob, options) return [...globs, ...CODE_FILE_LOADER_DEFAULT_IGNORES] } CodeFileLoader.prototype[CODE_FILE_LOADER_PATCH_FLAG] = true } const blocksMap = /* @__PURE__ */ new Map() let onDiskConfig let onDiskConfigLoaded = false const RELEVANT_KEYWORDS = ['gql', 'graphql', 'GraphQL'] /** Регулярний вираз на рівні модуля для e18e/prefer-static-regex. */ const VUE_OR_SVELTE_FILE_RE = /\.(vue|svelte)$/ /** Сегмент шляху до залежностей: виключаємо з пулу sibling-операцій для unique-operation-name / unique-fragment-name. */ const NODE_MODULES_SEGMENT_RE = /[/\\]node_modules[/\\]/ /** * Чи шлях джерела документа не всередині `node_modules`. * @param {{ filePath?: string }} entry запис операції/фрагмента з graphql-eslint * @returns {boolean} `true`, якщо `filePath` заданий і не містить сегмента `node_modules` */ function documentEntryOutsideNodeModules(entry) { return typeof entry?.filePath === 'string' && !NODE_MODULES_SEGMENT_RE.test(entry.filePath) } /** * Обгортка siblingOperations: graphql-config може підтягувати `documents` із `node_modules`, тоді * правила з `requiresSiblings` бачать дублікати імен між застосунком і залежністю. Фільтр лишає * лише записи зі шляхів поза `node_modules` (кеш плагіна не змінюється). * @param {object} siblings оригінальний `siblingOperations` з graphql-eslint parser services * @returns {object} той самий API з відфільтрованими `getOperation` / `getFragment` / списками */ function siblingOperationsWithoutNodeModules(siblings) { if (!siblings?.available) { return siblings } return { ...siblings, getFragment: name => siblings.getFragment(name).filter(entry => documentEntryOutsideNodeModules(entry)), getFragments: () => siblings.getFragments().filter(entry => documentEntryOutsideNodeModules(entry)), getFragmentByType: typeName => siblings.getFragmentByType(typeName).filter(entry => documentEntryOutsideNodeModules(entry)), getOperation: name => siblings.getOperation(name).filter(entry => documentEntryOutsideNodeModules(entry)), getOperations: () => siblings.getOperations().filter(entry => documentEntryOutsideNodeModules(entry)), getOperationByType: type => siblings.getOperationByType(type).filter(entry => documentEntryOutsideNodeModules(entry)) } } /** * Шлях для graphql-config: для віртуальних блоків Vue — фізичний .vue, інакше той самий filePath. * @param {string} filePath шлях файлу, який обробляє processor * @returns {string} шлях для loadOnDiskGraphQLConfig та getProjectForFile */ export function graphQLConfigPathAnchor(filePath = '') { if (filePath.includes('.vue/')) { return filePath.slice(0, filePath.indexOf('.vue/') + '.vue'.length) } return filePath } /** Processor як у graphql-eslint, зі стабільним anchor для graphql-config. */ export const graphqlCodefileProcessor = { meta: { name: '@nitra/graphql-codefile-processor', version }, supportsAutofix: true, preprocess(code, filePath) { if (env.ESLINT_USE_FLAT_CONFIG !== 'false' && filePath.endsWith('.vue')) { throw new Error( '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' ) } if (!onDiskConfigLoaded) { onDiskConfig = loadOnDiskGraphQLConfig(graphQLConfigPathAnchor(filePath)) onDiskConfigLoaded = true } let keywords = RELEVANT_KEYWORDS const anchor = graphQLConfigPathAnchor(filePath) const pluckConfig = onDiskConfig?.getProjectForFile(anchor)?.extensions?.pluckConfig if (pluckConfig) { const { modules = [], globalGqlIdentifierName = ['gql', 'graphql'], gqlMagicComment = 'GraphQL' } = pluckConfig const mods = modules.map(({ identifier }) => identifier).filter(v => !!v) const result = [...mods, ...asArray(globalGqlIdentifierName), gqlMagicComment] keywords = [...new Set(result)] } if (keywords.every(keyword => !code.includes(keyword))) { return [code] } try { const sources = gqlPluckFromCodeStringSync(filePath, code, { skipIndent: true, ...pluckConfig }) const blocks = sources.map(item => ({ filename: 'document.graphql', text: item.body, lineOffset: item.locationOffset.line - 1, offset: item.locationOffset.index + 1 })) blocksMap.set(filePath, blocks) return [...blocks, code] } catch (error) { if (error instanceof Error) { error.message = `[graphql-eslint] Error while preprocessing "${relative(CWD, filePath)}" file ${error.message}` } console.error(error) return [code] } }, postprocess(messages, filePath) { const blocks = blocksMap.get(filePath) || [] for (const [i, block] of blocks.entries()) { const { lineOffset, offset } = block for (const message of messages[i] || []) { const isVueOrSvelte = VUE_OR_SVELTE_FILE_RE.test(filePath) if (isVueOrSvelte) { delete message.endLine delete message.endColumn delete message.fix delete message.suggestions Object.assign(message, REPORT_ON_FIRST_CHARACTER) continue } message.line += lineOffset if (typeof message.endLine === 'number') { message.endLine += lineOffset } if (message.fix) { message.fix.range[0] += offset message.fix.range[1] += offset } for (const suggestion of message.suggestions || []) { const [start, end] = suggestion.fix.range suggestion.fix.range = [start + offset, end + offset] } } } const result = messages.flat() return result.toSorted((a, b) => a.line - b.line || a.column - b.column) } } const baseParser = graphqlEslintPlugin.parser /** * Парсер graphql-eslint: той самий anchor для filePath, щоб loadGraphQLConfig знаходив проєкт для віртуальних document.graphql з-під Vue. * @param {string} code вміст GraphQL-документа * @param {import('eslint').Linter.ParserOptions} options опції ESLint (filePath тощо) * @returns {ReturnType<typeof baseParser.parseForESLint>} AST і parserServices для graphql-eslint */ export function graphqlParseForESLintWithAnchor(code, options = {}) { const filePath = options.filePath || '' const anchoredFilePath = graphQLConfigPathAnchor(filePath) const parsed = baseParser.parseForESLint(code, { ...options, filePath: anchoredFilePath }) const sibs = parsed.services?.siblingOperations if (sibs) { parsed.services.siblingOperations = siblingOperationsWithoutNodeModules(sibs) } return parsed } /** Парсер для flat config (обгортка над graphql-eslint). */ export const graphqlParserWithConfigAnchor = { meta: baseParser.meta, parseForESLint: graphqlParseForESLintWithAnchor }