UNPKG

graphql-language-service-server

Version:
562 lines 23.3 kB
import * as fs from 'fs'; import { Kind, extendSchema, parse, visit } from 'graphql'; import nullthrows from 'nullthrows'; import { loadConfig, } from 'graphql-config'; import stringToHash from './stringToHash'; import glob from 'glob'; import { fileURLToPath, pathToFileURL } from 'url'; const MAX_READS = 200; const { DOCUMENT, FRAGMENT_DEFINITION, OBJECT_TYPE_DEFINITION, INTERFACE_TYPE_DEFINITION, ENUM_TYPE_DEFINITION, UNION_TYPE_DEFINITION, SCALAR_TYPE_DEFINITION, INPUT_OBJECT_TYPE_DEFINITION, SCALAR_TYPE_EXTENSION, OBJECT_TYPE_EXTENSION, INTERFACE_TYPE_EXTENSION, UNION_TYPE_EXTENSION, ENUM_TYPE_EXTENSION, INPUT_OBJECT_TYPE_EXTENSION, DIRECTIVE_DEFINITION, } = Kind; export async function getGraphQLCache({ parser, loadConfigOptions, config, }) { let graphQLConfig = config; if (!graphQLConfig) { graphQLConfig = await loadConfig(loadConfigOptions); } return new GraphQLCache({ configDir: loadConfigOptions.rootDir, config: graphQLConfig, parser, }); } export class GraphQLCache { constructor({ configDir, config, parser, }) { this.getGraphQLConfig = () => this._graphQLConfig; this.getProjectForFile = (uri) => { return this._graphQLConfig.getProjectForFile(fileURLToPath(uri)); }; this.getFragmentDependencies = async (query, fragmentDefinitions) => { if (!fragmentDefinitions) { return []; } let parsedQuery; try { parsedQuery = parse(query, { allowLegacySDLImplementsInterfaces: true, allowLegacySDLEmptyFields: true, }); } catch (error) { return []; } return this.getFragmentDependenciesForAST(parsedQuery, fragmentDefinitions); }; this.getFragmentDependenciesForAST = async (parsedQuery, fragmentDefinitions) => { if (!fragmentDefinitions) { return []; } const existingFrags = new Map(); const referencedFragNames = new Set(); visit(parsedQuery, { FragmentDefinition(node) { existingFrags.set(node.name.value, true); }, FragmentSpread(node) { if (!referencedFragNames.has(node.name.value)) { referencedFragNames.add(node.name.value); } }, }); const asts = new Set(); referencedFragNames.forEach(name => { if (!existingFrags.has(name) && fragmentDefinitions.has(name)) { asts.add(nullthrows(fragmentDefinitions.get(name))); } }); const referencedFragments = []; asts.forEach(ast => { visit(ast.definition, { FragmentSpread(node) { if (!referencedFragNames.has(node.name.value) && fragmentDefinitions.get(node.name.value)) { asts.add(nullthrows(fragmentDefinitions.get(node.name.value))); referencedFragNames.add(node.name.value); } }, }); if (!existingFrags.has(ast.definition.name.value)) { referencedFragments.push(ast); } }); return referencedFragments; }; this.getFragmentDefinitions = async (projectConfig) => { const rootDir = projectConfig.dirpath; if (this._fragmentDefinitionsCache.has(rootDir)) { return this._fragmentDefinitionsCache.get(rootDir) || new Map(); } const list = await this._readFilesFromInputDirs(rootDir, projectConfig); const { fragmentDefinitions, graphQLFileMap, } = await this.readAllGraphQLFiles(list); this._fragmentDefinitionsCache.set(rootDir, fragmentDefinitions); this._graphQLFileListCache.set(rootDir, graphQLFileMap); return fragmentDefinitions; }; this.getObjectTypeDependencies = async (query, objectTypeDefinitions) => { if (!objectTypeDefinitions) { return []; } let parsedQuery; try { parsedQuery = parse(query, { allowLegacySDLImplementsInterfaces: true, allowLegacySDLEmptyFields: true, }); } catch (error) { return []; } return this.getObjectTypeDependenciesForAST(parsedQuery, objectTypeDefinitions); }; this.getObjectTypeDependenciesForAST = async (parsedQuery, objectTypeDefinitions) => { if (!objectTypeDefinitions) { return []; } const existingObjectTypes = new Map(); const referencedObjectTypes = new Set(); visit(parsedQuery, { ObjectTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, InputObjectTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, EnumTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, NamedType(node) { if (!referencedObjectTypes.has(node.name.value)) { referencedObjectTypes.add(node.name.value); } }, }); const asts = new Set(); referencedObjectTypes.forEach(name => { if (!existingObjectTypes.has(name) && objectTypeDefinitions.has(name)) { asts.add(nullthrows(objectTypeDefinitions.get(name))); } }); const referencedObjects = []; asts.forEach(ast => { visit(ast.definition, { NamedType(node) { if (!referencedObjectTypes.has(node.name.value) && objectTypeDefinitions.get(node.name.value)) { asts.add(nullthrows(objectTypeDefinitions.get(node.name.value))); referencedObjectTypes.add(node.name.value); } }, }); if (!existingObjectTypes.has(ast.definition.name.value)) { referencedObjects.push(ast); } }); return referencedObjects; }; this.getObjectTypeDefinitions = async (projectConfig) => { const rootDir = projectConfig.dirpath; if (this._typeDefinitionsCache.has(rootDir)) { return this._typeDefinitionsCache.get(rootDir) || new Map(); } const list = await this._readFilesFromInputDirs(rootDir, projectConfig); const { objectTypeDefinitions, graphQLFileMap, } = await this.readAllGraphQLFiles(list); this._typeDefinitionsCache.set(rootDir, objectTypeDefinitions); this._graphQLFileListCache.set(rootDir, graphQLFileMap); return objectTypeDefinitions; }; this._readFilesFromInputDirs = (rootDir, projectConfig) => { let pattern; const { documents } = projectConfig; if (!documents || documents.length === 0) { return Promise.resolve([]); } if (typeof documents === 'string') { pattern = documents; } else if (documents.length === 1) { pattern = documents[0]; } else { pattern = `{${documents.join(',')}}`; } return new Promise((resolve, reject) => { const globResult = new glob.Glob(pattern, { cwd: rootDir, stat: true, absolute: false, ignore: [ 'generated/relay', '**/__flow__/**', '**/__generated__/**', '**/__github__/**', '**/__mocks__/**', '**/node_modules/**', '**/__flowtests__/**', ], }, error => { if (error) { reject(error); } }); globResult.on('end', () => { resolve(Object.keys(globResult.statCache) .filter(filePath => typeof globResult.statCache[filePath] === 'object') .filter(filePath => projectConfig.match(filePath)) .map(filePath => { const cacheEntry = globResult.statCache[filePath]; return { filePath: pathToFileURL(filePath).toString(), mtime: Math.trunc(cacheEntry.mtime.getTime() / 1000), size: cacheEntry.size, }; })); }); }); }; this.getSchema = async (appName, queryHasExtensions) => { var _a; const projectConfig = this._graphQLConfig.getProject(appName); if (!projectConfig) { return null; } const schemaPath = projectConfig.schema; const schemaKey = this._getSchemaCacheKeyForProject(projectConfig); let schemaCacheKey = null; let schema = null; if (!schema && schemaPath && schemaKey) { schemaCacheKey = schemaKey; if (this._schemaMap.has(schemaCacheKey)) { schema = this._schemaMap.get(schemaCacheKey); if (schema) { return queryHasExtensions ? this._extendSchema(schema, schemaPath, schemaCacheKey) : schema; } } schema = await projectConfig.getSchema(); } const customDirectives = (_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.extensions) === null || _a === void 0 ? void 0 : _a.customDirectives; if (customDirectives && schema) { const directivesSDL = customDirectives.join('\n\n'); schema = extendSchema(schema, parse(directivesSDL, { allowLegacySDLImplementsInterfaces: true, allowLegacySDLEmptyFields: true, })); } if (!schema) { return null; } if (this._graphQLFileListCache.has(this._configDir)) { schema = this._extendSchema(schema, schemaPath, schemaCacheKey); } if (schemaCacheKey) { this._schemaMap.set(schemaCacheKey, schema); } return schema; }; this.readAllGraphQLFiles = async (list) => { const queue = list.slice(); const responses = []; while (queue.length) { const chunk = queue.splice(0, MAX_READS); const promises = chunk.map(fileInfo => this.promiseToReadGraphQLFile(fileInfo.filePath) .catch(error => { console.log('pro', error); if (error.code === 'EMFILE' || error.code === 'ENFILE') { queue.push(fileInfo); } }) .then((response) => { if (response) { responses.push({ ...response, mtime: fileInfo.mtime, size: fileInfo.size, }); } })); await Promise.all(promises); } return this.processGraphQLFiles(responses); }; this.processGraphQLFiles = (responses) => { const objectTypeDefinitions = new Map(); const fragmentDefinitions = new Map(); const graphQLFileMap = new Map(); responses.forEach(response => { const { filePath, content, asts, mtime, size } = response; if (asts) { asts.forEach(ast => { ast.definitions.forEach(definition => { if (definition.kind === FRAGMENT_DEFINITION) { fragmentDefinitions.set(definition.name.value, { filePath, content, definition, }); } if (definition.kind === OBJECT_TYPE_DEFINITION || definition.kind === INPUT_OBJECT_TYPE_DEFINITION || definition.kind === ENUM_TYPE_DEFINITION) { objectTypeDefinitions.set(definition.name.value, { filePath, content, definition, }); } }); }); } graphQLFileMap.set(filePath, { filePath, content, asts, mtime, size, }); }); return { objectTypeDefinitions, fragmentDefinitions, graphQLFileMap, }; }; this.promiseToReadGraphQLFile = (filePath) => { return new Promise((resolve, reject) => fs.readFile(fileURLToPath(filePath), 'utf8', (error, content) => { if (error) { reject(error); return; } const asts = []; let queries = []; if (content.trim().length !== 0) { try { queries = this._parser(content, filePath); if (queries.length === 0) { resolve({ filePath, content, asts: [], queries: [], mtime: 0, size: 0, }); return; } queries.forEach(({ query }) => asts.push(parse(query, { allowLegacySDLImplementsInterfaces: true, allowLegacySDLEmptyFields: true, }))); resolve({ filePath, content, asts, queries, mtime: 0, size: 0, }); } catch (_) { resolve({ filePath, content, asts: [], queries: [], mtime: 0, size: 0, }); return; } } resolve({ filePath, content, asts, queries, mtime: 0, size: 0 }); })); }; this._configDir = configDir; this._graphQLConfig = config; this._graphQLFileListCache = new Map(); this._schemaMap = new Map(); this._fragmentDefinitionsCache = new Map(); this._typeDefinitionsCache = new Map(); this._typeExtensionMap = new Map(); this._parser = parser; } async _updateGraphQLFileListCache(graphQLFileMap, metrics, filePath, exists) { const fileAndContent = exists ? await this.promiseToReadGraphQLFile(filePath) : null; const existingFile = graphQLFileMap.get(filePath); if (existingFile && !exists) { graphQLFileMap.delete(filePath); } else if (fileAndContent) { const graphQLFileInfo = { ...fileAndContent, ...metrics }; graphQLFileMap.set(filePath, graphQLFileInfo); } return graphQLFileMap; } async updateFragmentDefinition(rootDir, filePath, contents) { const cache = this._fragmentDefinitionsCache.get(rootDir); const asts = contents.map(({ query }) => { try { return { ast: parse(query, { allowLegacySDLImplementsInterfaces: true, allowLegacySDLEmptyFields: true, }), query, }; } catch (error) { return { ast: null, query }; } }); if (cache) { cache.forEach((value, key) => { if (value.filePath === filePath) { cache.delete(key); } }); asts.forEach(({ ast, query }) => { if (!ast) { return; } ast.definitions.forEach(definition => { if (definition.kind === FRAGMENT_DEFINITION) { cache.set(definition.name.value, { filePath, content: query, definition, }); } }); }); } } async updateFragmentDefinitionCache(rootDir, filePath, exists) { const fileAndContent = exists ? await this.promiseToReadGraphQLFile(filePath) : null; if (!exists) { const cache = this._fragmentDefinitionsCache.get(rootDir); if (cache) { cache.delete(filePath); } } else if (fileAndContent && fileAndContent.queries) { this.updateFragmentDefinition(rootDir, filePath, fileAndContent.queries); } } async updateObjectTypeDefinition(rootDir, filePath, contents) { const cache = this._typeDefinitionsCache.get(rootDir); const asts = contents.map(({ query }) => { try { return { ast: parse(query, { allowLegacySDLImplementsInterfaces: true, allowLegacySDLEmptyFields: true, }), query, }; } catch (error) { return { ast: null, query }; } }); if (cache) { cache.forEach((value, key) => { if (value.filePath === filePath) { cache.delete(key); } }); asts.forEach(({ ast, query }) => { if (!ast) { return; } ast.definitions.forEach(definition => { if (definition.kind === OBJECT_TYPE_DEFINITION || definition.kind === INPUT_OBJECT_TYPE_DEFINITION || definition.kind === ENUM_TYPE_DEFINITION) { cache.set(definition.name.value, { filePath, content: query, definition, }); } }); }); } } async updateObjectTypeDefinitionCache(rootDir, filePath, exists) { const fileAndContent = exists ? await this.promiseToReadGraphQLFile(filePath) : null; if (!exists) { const cache = this._typeDefinitionsCache.get(rootDir); if (cache) { cache.delete(filePath); } } else if (fileAndContent && fileAndContent.queries) { this.updateObjectTypeDefinition(rootDir, filePath, fileAndContent.queries); } } _extendSchema(schema, schemaPath, schemaCacheKey) { const graphQLFileMap = this._graphQLFileListCache.get(this._configDir); const typeExtensions = []; if (!graphQLFileMap) { return schema; } graphQLFileMap.forEach(({ filePath, asts }) => { asts.forEach(ast => { if (filePath === schemaPath) { return; } ast.definitions.forEach(definition => { switch (definition.kind) { case OBJECT_TYPE_DEFINITION: case INTERFACE_TYPE_DEFINITION: case ENUM_TYPE_DEFINITION: case UNION_TYPE_DEFINITION: case SCALAR_TYPE_DEFINITION: case INPUT_OBJECT_TYPE_DEFINITION: case SCALAR_TYPE_EXTENSION: case OBJECT_TYPE_EXTENSION: case INTERFACE_TYPE_EXTENSION: case UNION_TYPE_EXTENSION: case ENUM_TYPE_EXTENSION: case INPUT_OBJECT_TYPE_EXTENSION: case DIRECTIVE_DEFINITION: typeExtensions.push(definition); break; } }); }); }); if (schemaCacheKey) { const sorted = typeExtensions.sort((a, b) => { const aName = a.definition ? a.definition.name.value : a.name.value; const bName = b.definition ? b.definition.name.value : b.name.value; return aName > bName ? 1 : -1; }); const hash = stringToHash(JSON.stringify(sorted)); if (this._typeExtensionMap.has(schemaCacheKey) && this._typeExtensionMap.get(schemaCacheKey) === hash) { return schema; } this._typeExtensionMap.set(schemaCacheKey, hash); } return extendSchema(schema, { kind: DOCUMENT, definitions: typeExtensions, }); } _invalidateSchemaCacheForProject(projectConfig) { const schemaKey = this._getSchemaCacheKeyForProject(projectConfig); schemaKey && this._schemaMap.delete(schemaKey); } _getSchemaCacheKeyForProject(projectConfig) { return projectConfig.schema; } _getProjectName(projectConfig) { return projectConfig || 'default'; } } //# sourceMappingURL=GraphQLCache.js.map