From a0ea67f773fafa55975631a4f1aac92c6f68e4d9 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Mon, 31 Oct 2022 14:58:50 +0100 Subject: [PATCH] feat: reload schema/documents cache (only for **current project**) in VSCode (#1222) * feat: reload schema/documents cache (only for **current project**) in VSCode * Update packages/plugin/src/documents.ts * Update packages/plugin/src/schema.ts --- .changeset/wet-ads-rush.md | 5 +++ .github/renovate.json | 7 +++- packages/plugin/package.json | 3 +- packages/plugin/src/cache.ts | 26 ++++++++++++++ .../{sibling-operations.ts => documents.ts} | 18 +++++----- packages/plugin/src/parser.ts | 30 +++++++--------- packages/plugin/src/rules/no-unused-fields.ts | 2 +- .../plugin/src/rules/selection-set-depth.ts | 2 +- .../plugin/src/rules/unique-fragment-name.ts | 6 ++-- packages/plugin/src/schema.ts | 28 +++++++-------- packages/plugin/src/types.ts | 14 ++++---- packages/plugin/src/utils.ts | 34 ++++--------------- packages/plugin/tests/schema.spec.ts | 2 +- 13 files changed, 93 insertions(+), 84 deletions(-) create mode 100644 .changeset/wet-ads-rush.md create mode 100644 packages/plugin/src/cache.ts rename packages/plugin/src/{sibling-operations.ts => documents.ts} (92%) diff --git a/.changeset/wet-ads-rush.md b/.changeset/wet-ads-rush.md new file mode 100644 index 000000000000..1ba2af0c2b3a --- /dev/null +++ b/.changeset/wet-ads-rush.md @@ -0,0 +1,5 @@ +--- +'@graphql-eslint/eslint-plugin': minor +--- + +feat: reload schema/documents cache (only for **current project**) in VSCode diff --git a/.github/renovate.json b/.github/renovate.json index 962502aea6ac..63454969424c 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,5 +1,10 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>the-guild-org/shared-config:renovate"], - "automerge": true + "lockFileMaintenance": { + "enabled": true, + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true + } } diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 388ad0af7d79..c724304005a5 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -92,7 +92,8 @@ "graphql/validation/rules/ValuesOfCorrectTypeRule", "graphql/validation/rules/VariablesAreInputTypesRule", "graphql/validation/rules/VariablesInAllowedPositionRule", - "graphql/language" + "graphql/language", + "minimatch" ] }, "publishConfig": { diff --git a/packages/plugin/src/cache.ts b/packages/plugin/src/cache.ts new file mode 100644 index 000000000000..95a5150aea39 --- /dev/null +++ b/packages/plugin/src/cache.ts @@ -0,0 +1,26 @@ +// Based on the `eslint-plugin-import`'s cache +// https://github.com/import-js/eslint-plugin-import/blob/main/utils/ModuleCache.js +import debugFactory from 'debug'; + +const log = debugFactory('graphql-eslint:ModuleCache'); + +export class ModuleCache { + map = new Map(); + + set(cacheKey: K, result: T): void { + this.map.set(cacheKey, { lastSeen: process.hrtime(), result }); + log('setting entry for', cacheKey); + } + + get(cacheKey, settings = { lifetime: 10 /* seconds */ }): void | T { + if (!this.map.has(cacheKey)) { + log('cache miss for', cacheKey); + return; + } + const { lastSeen, result } = this.map.get(cacheKey); + // check freshness + if (process.hrtime(lastSeen)[0] < settings.lifetime) { + return result; + } + } +} diff --git a/packages/plugin/src/sibling-operations.ts b/packages/plugin/src/documents.ts similarity index 92% rename from packages/plugin/src/sibling-operations.ts rename to packages/plugin/src/documents.ts index 3a82deacb92e..53732813dae2 100644 --- a/packages/plugin/src/sibling-operations.ts +++ b/packages/plugin/src/documents.ts @@ -7,12 +7,13 @@ import { visit, OperationTypeNode, } from 'graphql'; -import { Source, asArray } from '@graphql-tools/utils'; +import { Source } from '@graphql-tools/utils'; import { GraphQLProjectConfig } from 'graphql-config'; import debugFactory from 'debug'; -import fastGlob from 'fast-glob'; +import fg from 'fast-glob'; import { logger } from './utils'; -import type { Pointer } from './types'; +import { Pointer } from './types'; +import { ModuleCache } from './cache'; export type FragmentSource = { filePath: string; document: FragmentDefinitionNode }; export type OperationSource = { filePath: string; document: OperationDefinitionNode }; @@ -50,12 +51,11 @@ const handleVirtualPath = (documents: Source[]): Source[] => { }); }; -const operationsCache = new Map(); +const operationsCache = new ModuleCache(); const siblingOperationsCache = new Map(); const getSiblings = (project: GraphQLProjectConfig): Source[] => { - const documentsKey = asArray(project.documents).sort().join(','); - + const documentsKey = project.documents; if (!documentsKey) { return []; } @@ -70,9 +70,7 @@ const getSiblings = (project: GraphQLProjectConfig): Source[] => { }); if (debug.enabled) { debug('Loaded %d operations', documents.length); - const operationsPaths = fastGlob.sync(project.documents as Pointer, { - absolute: true, - }); + const operationsPaths = fg.sync(project.documents as Pointer, { absolute: true }); debug('Operations pointers %O', operationsPaths); } siblings = handleVirtualPath(documents); @@ -82,7 +80,7 @@ const getSiblings = (project: GraphQLProjectConfig): Source[] => { return siblings; }; -export function getSiblingOperations(project: GraphQLProjectConfig): SiblingOperations { +export function getDocuments(project: GraphQLProjectConfig): SiblingOperations { const siblings = getSiblings(project); if (siblings.length === 0) { diff --git a/packages/plugin/src/parser.ts b/packages/plugin/src/parser.ts index 429e021b6688..058ac26c2832 100644 --- a/packages/plugin/src/parser.ts +++ b/packages/plugin/src/parser.ts @@ -4,44 +4,38 @@ import debugFactory from 'debug'; import { convertToESTree, extractComments, extractTokens } from './estree-converter'; import { GraphQLESLintParseResult, ParserOptions } from './types'; import { getSchema } from './schema'; -import { getSiblingOperations } from './sibling-operations'; +import { getDocuments } from './documents'; import { loadGraphQLConfig } from './graphql-config'; -import { getOnDiskFilepath } from './utils'; +import { CWD, VIRTUAL_DOCUMENT_REGEX } from './utils'; const debug = debugFactory('graphql-eslint:parser'); -debug('cwd %o', process.cwd()); +debug('cwd %o', CWD); export function parseForESLint(code: string, options: ParserOptions): GraphQLESLintParseResult { try { const { filePath } = options; - const realFilepath = getOnDiskFilepath(filePath); - - const gqlConfig = loadGraphQLConfig(options); - const projectForFile = realFilepath - ? gqlConfig.getProjectForFile(realFilepath) - : gqlConfig.getDefault(); - - const schema = getSchema(projectForFile, options); - const siblingOperations = getSiblingOperations(projectForFile); - + // First parse code from file, in case of syntax error do not try load schema, + // documents or even graphql-config instance const { document } = parseGraphQLSDL(filePath, code, { ...options.graphQLParserOptions, noLocation: false, }); + const gqlConfig = loadGraphQLConfig(options); + const realFilepath = filePath.replace(VIRTUAL_DOCUMENT_REGEX, ''); + const project = gqlConfig.getProjectForFile(realFilepath); - const comments = extractComments(document.loc); - const tokens = extractTokens(filePath, code); + const schema = getSchema(project, options.schemaOptions); const rootTree = convertToESTree(document, schema instanceof GraphQLSchema ? schema : null); return { services: { schema, - siblingOperations, + siblingOperations: getDocuments(project), }, ast: { - comments, - tokens, + comments: extractComments(document.loc), + tokens: extractTokens(filePath, code), loc: rootTree.loc, range: rootTree.range as [number, number], type: 'Program', diff --git a/packages/plugin/src/rules/no-unused-fields.ts b/packages/plugin/src/rules/no-unused-fields.ts index 734292dd8659..2a57d73e79c5 100644 --- a/packages/plugin/src/rules/no-unused-fields.ts +++ b/packages/plugin/src/rules/no-unused-fields.ts @@ -1,7 +1,7 @@ import { GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql'; import { GraphQLESLintRule } from '../types'; import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils'; -import { SiblingOperations } from '../sibling-operations'; +import { SiblingOperations } from '../documents'; const RULE_ID = 'no-unused-fields'; diff --git a/packages/plugin/src/rules/selection-set-depth.ts b/packages/plugin/src/rules/selection-set-depth.ts index af3cbc17d634..359ff3642139 100644 --- a/packages/plugin/src/rules/selection-set-depth.ts +++ b/packages/plugin/src/rules/selection-set-depth.ts @@ -4,7 +4,7 @@ import depthLimit from 'graphql-depth-limit'; import { DocumentNode, ExecutableDefinitionNode, GraphQLError, Kind } from 'graphql'; import { GraphQLESTreeNode } from '../estree-converter'; import { ARRAY_DEFAULT_OPTIONS, logger, requireSiblingsOperations } from '../utils'; -import { SiblingOperations } from '../sibling-operations'; +import { SiblingOperations } from '../documents'; export type SelectionSetDepthRuleConfig = { maxDepth: number; ignore?: string[] }; diff --git a/packages/plugin/src/rules/unique-fragment-name.ts b/packages/plugin/src/rules/unique-fragment-name.ts index ee056aac403e..0a673cf799ab 100644 --- a/packages/plugin/src/rules/unique-fragment-name.ts +++ b/packages/plugin/src/rules/unique-fragment-name.ts @@ -2,8 +2,8 @@ import { relative } from 'path'; import { ExecutableDefinitionNode, Kind } from 'graphql'; import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types'; import { GraphQLESTreeNode } from '../estree-converter'; -import { normalizePath, requireSiblingsOperations, getOnDiskFilepath } from '../utils'; -import { FragmentSource, OperationSource } from '../sibling-operations'; +import { normalizePath, requireSiblingsOperations, VIRTUAL_DOCUMENT_REGEX, CWD } from '../utils'; +import { FragmentSource, OperationSource } from '../documents'; const RULE_ID = 'unique-fragment-name'; @@ -32,7 +32,7 @@ export const checkNode = ( data: { documentName, summary: conflictingDocuments - .map(f => `\t${relative(process.cwd(), getOnDiskFilepath(f.filePath))}`) + .map(f => `\t${relative(CWD, f.filePath.replace(VIRTUAL_DOCUMENT_REGEX, ''))}`) .join('\n'), }, node: node.name, diff --git a/packages/plugin/src/schema.ts b/packages/plugin/src/schema.ts index 597a1c5e53e3..93932f495773 100644 --- a/packages/plugin/src/schema.ts +++ b/packages/plugin/src/schema.ts @@ -1,26 +1,28 @@ import { GraphQLSchema } from 'graphql'; import { GraphQLProjectConfig } from 'graphql-config'; -import { asArray } from '@graphql-tools/utils'; import debugFactory from 'debug'; -import fastGlob from 'fast-glob'; +import fg from 'fast-glob'; import chalk from 'chalk'; -import type { ParserOptions, Schema, Pointer } from './types'; +import { ParserOptions, Schema, Pointer } from './types'; +import { ModuleCache } from './cache'; -const schemaCache = new Map(); +const schemaCache = new ModuleCache(); const debug = debugFactory('graphql-eslint:schema'); export function getSchema( project: GraphQLProjectConfig, - options: Omit = {}, + schemaOptions?: ParserOptions['schemaOptions'], ): Schema { - const schemaKey = asArray(project.schema).sort().join(','); + const schemaKey = project.schema; if (!schemaKey) { return null; } - if (schemaCache.has(schemaKey)) { - return schemaCache.get(schemaKey); + const cache = schemaCache.get(schemaKey); + + if (cache) { + return cache; } let schema: Schema; @@ -28,21 +30,19 @@ export function getSchema( try { debug('Loading schema from %o', project.schema); schema = project.loadSchemaSync(project.schema, 'GraphQLSchema', { - ...options.schemaOptions, + ...schemaOptions, pluckConfig: project.extensions.pluckConfig, }); if (debug.enabled) { debug('Schema loaded: %o', schema instanceof GraphQLSchema); - const schemaPaths = fastGlob.sync(project.schema as Pointer, { - absolute: true, - }); + const schemaPaths = fg.sync(project.schema as Pointer, { absolute: true }); debug('Schema pointers %O', schemaPaths); } + // Do not set error to cache, since cache reload will be done after some `lifetime` seconds + schemaCache.set(schemaKey, schema); } catch (error) { error.message = chalk.red(`Error while loading schema: ${error.message}`); schema = error as Error; } - - schemaCache.set(schemaKey, schema); return schema; } diff --git a/packages/plugin/src/types.ts b/packages/plugin/src/types.ts index 60809983af4e..910b192c147b 100644 --- a/packages/plugin/src/types.ts +++ b/packages/plugin/src/types.ts @@ -1,10 +1,10 @@ -import type { Rule, AST, Linter } from 'eslint'; -import type * as ESTree from 'estree'; -import type { GraphQLSchema } from 'graphql'; -import type { IExtensions, IGraphQLProject } from 'graphql-config'; -import type { GraphQLParseOptions } from '@graphql-tools/utils'; -import type { GraphQLESLintRuleListener } from './testkit'; -import type { SiblingOperations } from './sibling-operations'; +import { Rule, AST, Linter } from 'eslint'; +import * as ESTree from 'estree'; +import { GraphQLSchema } from 'graphql'; +import { IExtensions, IGraphQLProject } from 'graphql-config'; +import { GraphQLParseOptions } from '@graphql-tools/utils'; +import { GraphQLESLintRuleListener } from './testkit'; +import { SiblingOperations } from './documents'; export type Schema = GraphQLSchema | Error | null; export type Pointer = string | string[]; diff --git a/packages/plugin/src/utils.ts b/packages/plugin/src/utils.ts index 1179a56e031e..7758b746b93f 100644 --- a/packages/plugin/src/utils.ts +++ b/packages/plugin/src/utils.ts @@ -1,13 +1,10 @@ -import { statSync } from 'fs'; -import { dirname } from 'path'; -import type { GraphQLSchema } from 'graphql'; -import { Kind } from 'graphql'; -import type { AST } from 'eslint'; +import { GraphQLSchema, Kind } from 'graphql'; +import { AST } from 'eslint'; import lowerCase from 'lodash.lowercase'; import chalk from 'chalk'; -import type { Position } from 'estree'; -import type { GraphQLESLintRuleContext } from './types'; -import type { SiblingOperations } from './sibling-operations'; +import { Position } from 'estree'; +import { GraphQLESLintRuleContext } from './types'; +import { SiblingOperations } from './documents'; export function requireSiblingsOperations( ruleId: string, @@ -46,26 +43,9 @@ export const logger = { export const normalizePath = (path: string): string => (path || '').replace(/\\/g, '/'); -/** - * https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71 - * Given a filepath, get the nearest path that is a regular file. - * The filepath provided by eslint may be a virtual filepath rather than a file - * on disk. This attempts to transform a virtual path into an on-disk path - */ -export const getOnDiskFilepath = (filepath: string): string => { - try { - if (statSync(filepath).isFile()) { - return filepath; - } - } catch (err) { - // https://github.com/eslint/eslint/issues/11989 - if (err.code === 'ENOTDIR') { - return getOnDiskFilepath(dirname(filepath)); - } - } +export const VIRTUAL_DOCUMENT_REGEX = /\/\d+_document.graphql$/; - return filepath; -}; +export const CWD = process.cwd(); export const getTypeName = (node): string => 'type' in node ? getTypeName(node.type) : node.name.value; diff --git a/packages/plugin/tests/schema.spec.ts b/packages/plugin/tests/schema.spec.ts index 107e1d239948..ca7ce7dde703 100644 --- a/packages/plugin/tests/schema.spec.ts +++ b/packages/plugin/tests/schema.spec.ts @@ -97,7 +97,7 @@ describe('schema', () => { // https://github.com/B2o5T/graphql-eslint/blob/master/docs/parser-options.md#schemaoptions it('with `parserOptions.schemaOptions`', () => { const gqlConfig = loadGraphQLConfig({ schema: schemaUrl, filePath: '' }); - const error = getSchema(gqlConfig.getDefault(), { schemaOptions }) as Error; + const error = getSchema(gqlConfig.getDefault(), schemaOptions) as Error; expect(error).toBeInstanceOf(Error); expect(error.message).toMatch('"authorization":"Bearer Foo"'); });