UNPKG

graphql-language-service-server

Version:
569 lines 24.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphQLCache = exports.getGraphQLCache = void 0; const graphql_1 = require("graphql"); const promises_1 = require("node:fs/promises"); const nullthrows_1 = __importDefault(require("nullthrows")); const graphql_config_1 = require("graphql-config"); const stringToHash_1 = __importDefault(require("./stringToHash")); const glob_1 = __importDefault(require("glob")); const vscode_uri_1 = require("vscode-uri"); const code_file_loader_1 = require("@graphql-tools/code-file-loader"); const constants_1 = require("./constants"); const lru_cache_1 = require("lru-cache"); const codeLoaderConfig = { noSilentErrors: false, pluckConfig: { skipIndent: true, }, }; const LanguageServiceExtension = api => { api.loaders.schema.register(new code_file_loader_1.CodeFileLoader(codeLoaderConfig)); api.loaders.documents.register(new code_file_loader_1.CodeFileLoader(codeLoaderConfig)); return { name: 'languageService' }; }; const MAX_READS = 200; async function getGraphQLCache({ parser, logger, loadConfigOptions, config, onSchemaChange, schemaCacheTTL, }) { var _a, _b, _c; const graphQLConfig = config || (await (0, graphql_config_1.loadConfig)({ ...loadConfigOptions, extensions: [ ...((_a = loadConfigOptions === null || loadConfigOptions === void 0 ? void 0 : loadConfigOptions.extensions) !== null && _a !== void 0 ? _a : []), LanguageServiceExtension, ], })); return new GraphQLCache({ configDir: loadConfigOptions.rootDir, config: graphQLConfig, parser, logger, onSchemaChange, schemaCacheTTL: schemaCacheTTL !== null && schemaCacheTTL !== void 0 ? schemaCacheTTL : (_c = (_b = config === null || config === void 0 ? void 0 : config.extensions) === null || _b === void 0 ? void 0 : _b.get('languageService')) === null || _c === void 0 ? void 0 : _c.schemaCacheTTL, }); } exports.getGraphQLCache = getGraphQLCache; class GraphQLCache { constructor({ configDir, config, parser, logger, onSchemaChange, schemaCacheTTL, }) { this.getGraphQLConfig = () => this._graphQLConfig; this.getProjectForFile = (uri) => { try { const project = this._graphQLConfig.getProjectForFile(vscode_uri_1.URI.parse(uri).fsPath); if (!project.documents) { this._logger.warn(`No documents configured for project ${project.name}. Many features will not work correctly.`); } return project; } catch (err) { this._logger.error(`there was an error loading the project config for this file ${err}`); return; } }; this.getFragmentDependencies = async (query, fragmentDefinitions) => { if (!fragmentDefinitions) { return []; } let parsedQuery; try { parsedQuery = (0, graphql_1.parse)(query); } catch (_a) { return []; } return this.getFragmentDependenciesForAST(parsedQuery, fragmentDefinitions); }; this.getFragmentDependenciesForAST = async (parsedQuery, fragmentDefinitions) => { if (!fragmentDefinitions) { return []; } const existingFrags = new Map(); const referencedFragNames = new Set(); (0, graphql_1.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(); for (const name of referencedFragNames) { if (!existingFrags.has(name) && fragmentDefinitions.has(name)) { asts.add((0, nullthrows_1.default)(fragmentDefinitions.get(name))); } } const referencedFragments = []; for (const ast of asts) { (0, graphql_1.visit)(ast.definition, { FragmentSpread(node) { if (!referencedFragNames.has(node.name.value) && fragmentDefinitions.get(node.name.value)) { asts.add((0, nullthrows_1.default)(fragmentDefinitions.get(node.name.value))); referencedFragNames.add(node.name.value); } }, }); if (!existingFrags.has(ast.definition.name.value)) { referencedFragments.push(ast); } } return referencedFragments; }; this._cacheKeyForProject = ({ dirpath, name }) => { return `${dirpath}-${name}`; }; this.getFragmentDefinitions = async (projectConfig) => { const rootDir = projectConfig.dirpath; const cacheKey = this._cacheKeyForProject(projectConfig); if (this._fragmentDefinitionsCache.has(cacheKey)) { return this._fragmentDefinitionsCache.get(cacheKey) || new Map(); } const list = await this._readFilesFromInputDirs(rootDir, projectConfig); const { fragmentDefinitions, graphQLFileMap } = await this.readAllGraphQLFiles(list); this._fragmentDefinitionsCache.set(cacheKey, fragmentDefinitions); this._graphQLFileListCache.set(cacheKey, graphQLFileMap); return fragmentDefinitions; }; this.getObjectTypeDependenciesForAST = async (parsedQuery, objectTypeDefinitions) => { if (!objectTypeDefinitions) { return []; } const existingObjectTypes = new Map(); const referencedObjectTypes = new Set(); (0, graphql_1.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); } }, UnionTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, ScalarTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, InterfaceTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, }); const asts = new Set(); for (const name of referencedObjectTypes) { if (!existingObjectTypes.has(name) && objectTypeDefinitions.has(name)) { asts.add((0, nullthrows_1.default)(objectTypeDefinitions.get(name))); } } const referencedObjects = []; for (const ast of asts) { (0, graphql_1.visit)(ast.definition, { NamedType(node) { if (!referencedObjectTypes.has(node.name.value) && objectTypeDefinitions.get(node.name.value)) { asts.add((0, nullthrows_1.default)(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; const cacheKey = this._cacheKeyForProject(projectConfig); if (this._typeDefinitionsCache.has(cacheKey)) { return this._typeDefinitionsCache.get(cacheKey) || new Map(); } const list = await this._readFilesFromInputDirs(rootDir, projectConfig); const { objectTypeDefinitions, graphQLFileMap } = await this.readAllGraphQLFiles(list); this._typeDefinitionsCache.set(cacheKey, objectTypeDefinitions); this._graphQLFileListCache.set(cacheKey, graphQLFileMap); return objectTypeDefinitions; }; this._readFilesFromInputDirs = (rootDir, projectConfig) => { let pattern; const patterns = this._getSchemaAndDocumentFilePatterns(projectConfig); if (patterns.length === 1) { pattern = patterns[0]; } else { pattern = `{${patterns.join(',')}}`; } return new Promise((resolve, reject) => { const globResult = new glob_1.default.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: vscode_uri_1.URI.file(filePath).toString(), mtime: Math.trunc(cacheEntry.mtime.getTime() / 1000), size: cacheEntry.size, }; })); }); }); }; this._getSchemaAndDocumentFilePatterns = (projectConfig) => { const patterns = []; for (const pointer of [projectConfig.documents, projectConfig.schema]) { if (pointer) { if (typeof pointer === 'string') { patterns.push(pointer); } else if (Array.isArray(pointer)) { patterns.push(...pointer); } } } return patterns; }; 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 (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 = (0, graphql_1.extendSchema)(schema, (0, graphql_1.parse)(directivesSDL)); } if (!schema) { return null; } if (this._graphQLFileListCache.has(this._configDir)) { schema = this._extendSchema(schema, schemaPath, schemaCacheKey); } if (schemaCacheKey) { this._schemaMap.set(schemaCacheKey, schema); if (this._onSchemaChange) { this._onSchemaChange(projectConfig); } } 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(async (fileInfo) => { try { const response = await this.promiseToReadGraphQLFile(fileInfo.filePath); responses.push({ ...response, mtime: fileInfo.mtime, size: fileInfo.size, }); } catch (error) { console.log('pro', error); if (error.code === 'EMFILE' || error.code === 'ENFILE') { queue.push(fileInfo); } } }); await Promise.all(promises); } return this.processGraphQLFiles(responses); }; this.processGraphQLFiles = (responses) => { const objectTypeDefinitions = new Map(); const fragmentDefinitions = new Map(); const graphQLFileMap = new Map(); for (const response of responses) { const { filePath, content, asts, mtime, size } = response; if (asts) { for (const ast of asts) { for (const definition of ast.definitions) { if (definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) { fragmentDefinitions.set(definition.name.value, { filePath, content, definition, }); } else if ((0, graphql_1.isTypeDefinitionNode)(definition)) { objectTypeDefinitions.set(definition.name.value, { filePath, content, definition, }); } } } } graphQLFileMap.set(filePath, { filePath, content, asts, mtime, size, }); } return { objectTypeDefinitions, fragmentDefinitions, graphQLFileMap, }; }; this.promiseToReadGraphQLFile = async (filePath) => { const content = await (0, promises_1.readFile)(vscode_uri_1.URI.parse(filePath).fsPath, 'utf-8'); const asts = []; let queries = []; if (content.trim().length !== 0) { try { queries = await this._parser(content, filePath, constants_1.DEFAULT_SUPPORTED_EXTENSIONS, constants_1.DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, this._logger); if (queries.length === 0) { return { filePath, content, asts: [], queries: [], mtime: 0, size: 0, }; } for (const { query } of queries) { asts.push((0, graphql_1.parse)(query)); } return { filePath, content, asts, queries, mtime: 0, size: 0, }; } catch (_a) { return { filePath, content, asts: [], queries: [], mtime: 0, size: 0, }; } } return { filePath, content, asts, queries, mtime: 0, size: 0 }; }; this._configDir = configDir; this._graphQLConfig = config; this._graphQLFileListCache = new Map(); this._schemaMap = new lru_cache_1.LRUCache({ max: 20, ttl: schemaCacheTTL !== null && schemaCacheTTL !== void 0 ? schemaCacheTTL : 1000 * 30, ttlAutopurge: true, updateAgeOnGet: false, }); this._fragmentDefinitionsCache = new Map(); this._typeDefinitionsCache = new Map(); this._typeExtensionMap = new Map(); this._parser = parser; this._logger = logger; this._onSchemaChange = onSchemaChange; } async updateFragmentDefinition(projectCacheKey, filePath, contents) { const cache = this._fragmentDefinitionsCache.get(projectCacheKey); const asts = contents.map(({ query }) => { try { return { ast: (0, graphql_1.parse)(query), query, }; } catch (_a) { return { ast: null, query }; } }); if (cache) { for (const [key, value] of cache.entries()) { if (value.filePath === filePath) { cache.delete(key); } } this._setFragmentCache(asts, cache, filePath); } else { const newFragmentCache = this._setFragmentCache(asts, new Map(), filePath); this._fragmentDefinitionsCache.set(projectCacheKey, newFragmentCache); } } _setFragmentCache(asts, fragmentCache, filePath) { for (const { ast, query } of asts) { if (!ast) { continue; } for (const definition of ast.definitions) { if (definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) { fragmentCache.set(definition.name.value, { filePath, content: query, definition, }); } } } return fragmentCache; } async updateObjectTypeDefinition(projectCacheKey, filePath, contents) { const cache = this._typeDefinitionsCache.get(projectCacheKey); const asts = contents.map(({ query }) => { try { return { ast: (0, graphql_1.parse)(query), query, }; } catch (_a) { return { ast: null, query }; } }); if (cache) { for (const [key, value] of cache.entries()) { if (value.filePath === filePath) { cache.delete(key); } } this._setDefinitionCache(asts, cache, filePath); } else { const newTypeCache = this._setDefinitionCache(asts, new Map(), filePath); this._typeDefinitionsCache.set(projectCacheKey, newTypeCache); } } _setDefinitionCache(asts, typeCache, filePath) { for (const { ast, query } of asts) { if (!ast) { continue; } for (const definition of ast.definitions) { if ((0, graphql_1.isTypeDefinitionNode)(definition)) { typeCache.set(definition.name.value, { filePath, content: query, definition, }); } } } return typeCache; } _extendSchema(schema, schemaPath, schemaCacheKey) { const graphQLFileMap = this._graphQLFileListCache.get(this._configDir); const typeExtensions = []; if (!graphQLFileMap) { return schema; } for (const { filePath, asts } of graphQLFileMap.values()) { for (const ast of asts) { if (filePath === schemaPath) { continue; } for (const definition of ast.definitions) { switch (definition.kind) { case graphql_1.Kind.OBJECT_TYPE_DEFINITION: case graphql_1.Kind.INTERFACE_TYPE_DEFINITION: case graphql_1.Kind.ENUM_TYPE_DEFINITION: case graphql_1.Kind.UNION_TYPE_DEFINITION: case graphql_1.Kind.SCALAR_TYPE_DEFINITION: case graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION: case graphql_1.Kind.SCALAR_TYPE_EXTENSION: case graphql_1.Kind.OBJECT_TYPE_EXTENSION: case graphql_1.Kind.INTERFACE_TYPE_EXTENSION: case graphql_1.Kind.UNION_TYPE_EXTENSION: case graphql_1.Kind.ENUM_TYPE_EXTENSION: case graphql_1.Kind.INPUT_OBJECT_TYPE_EXTENSION: case graphql_1.Kind.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 = (0, stringToHash_1.default)(JSON.stringify(sorted)); if (this._typeExtensionMap.has(schemaCacheKey) && this._typeExtensionMap.get(schemaCacheKey) === hash) { return schema; } this._typeExtensionMap.set(schemaCacheKey, hash); } return (0, graphql_1.extendSchema)(schema, { kind: graphql_1.Kind.DOCUMENT, definitions: typeExtensions, }); } invalidateSchemaCacheForProject(projectConfig) { const schemaKey = this._getSchemaCacheKeyForProject(projectConfig); if (schemaKey) { this._schemaMap.delete(schemaKey); } } _getSchemaCacheKeyForProject(projectConfig) { return projectConfig.schema; } _getProjectName(projectConfig) { return (projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.name) || 'default'; } } exports.GraphQLCache = GraphQLCache; //# sourceMappingURL=GraphQLCache.js.map