Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reload schema/documents cache (only for **current project**) in VSCode #1222

Merged
merged 3 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>();
export 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