Skip to content

Commit

Permalink
feat: reload schema/documents cache (only for **current project**) in…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
dimaMachina committed Oct 31, 2022
1 parent 8568313 commit cf59b0a
Show file tree
Hide file tree
Showing 13 changed files with 93 additions and 84 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-ads-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

feat: reload schema/documents cache (only for **current project**) in VSCode
7 changes: 6 additions & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 2 additions & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
"graphql/validation/rules/ValuesOfCorrectTypeRule",
"graphql/validation/rules/VariablesAreInputTypesRule",
"graphql/validation/rules/VariablesInAllowedPositionRule",
"graphql/language"
"graphql/language",
"minimatch"
]
},
"publishConfig": {
Expand Down
26 changes: 26 additions & 0 deletions packages/plugin/src/cache.ts
Original file line number Diff line number Diff line change
@@ -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<T, K = any> {
map = new Map<K, { lastSeen: [number, number]; result: T }>();

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -50,12 +51,11 @@ const handleVirtualPath = (documents: Source[]): Source[] => {
});
};

const operationsCache = new Map<string, Source[]>();
const operationsCache = new ModuleCache<Source[]>();
const siblingOperationsCache = new Map<Source[], SiblingOperations>();

const getSiblings = (project: GraphQLProjectConfig): Source[] => {
const documentsKey = asArray(project.documents).sort().join(',');

const documentsKey = project.documents;
if (!documentsKey) {
return [];
}
Expand All @@ -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);
Expand All @@ -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) {
Expand Down
30 changes: 12 additions & 18 deletions packages/plugin/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/src/rules/no-unused-fields.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/src/rules/selection-set-depth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] };

Expand Down
6 changes: 3 additions & 3 deletions packages/plugin/src/rules/unique-fragment-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down
28 changes: 14 additions & 14 deletions packages/plugin/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
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<string, GraphQLSchema | Error>();
const schemaCache = new ModuleCache<GraphQLSchema | Error>();
const debug = debugFactory('graphql-eslint:schema');

export function getSchema(
project: GraphQLProjectConfig,
options: Omit<ParserOptions, 'filePath'> = {},
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;

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;
}
14 changes: 7 additions & 7 deletions packages/plugin/src/types.ts
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down
34 changes: 7 additions & 27 deletions packages/plugin/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/tests/schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
});
Expand Down

0 comments on commit cf59b0a

Please sign in to comment.