graphql-language-service-server
Version:
Server process backing the GraphQL Language Service
561 lines • 23.4 kB
JavaScript
import { isTypeDefinitionNode, Kind, extendSchema, parse, visit, } from 'graphql';
import { readFile } from 'node:fs/promises';
import nullthrows from 'nullthrows';
import { loadConfig, } from 'graphql-config';
import stringToHash from './stringToHash';
import glob from 'glob';
import { URI } from 'vscode-uri';
import { CodeFileLoader, } from '@graphql-tools/code-file-loader';
import { DEFAULT_SUPPORTED_EXTENSIONS, DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, } from './constants';
import { LRUCache } from 'lru-cache';
const codeLoaderConfig = {
noSilentErrors: false,
pluckConfig: {
skipIndent: true,
},
};
const LanguageServiceExtension = api => {
api.loaders.schema.register(new CodeFileLoader(codeLoaderConfig));
api.loaders.documents.register(new CodeFileLoader(codeLoaderConfig));
return { name: 'languageService' };
};
const MAX_READS = 200;
export async function getGraphQLCache({ parser, logger, loadConfigOptions, config, onSchemaChange, schemaCacheTTL, }) {
var _a, _b, _c;
const graphQLConfig = config ||
(await 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,
});
}
export class GraphQLCache {
constructor({ configDir, config, parser, logger, onSchemaChange, schemaCacheTTL, }) {
this.getGraphQLConfig = () => this._graphQLConfig;
this.getProjectForFile = (uri) => {
try {
const project = this._graphQLConfig.getProjectForFile(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 = 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();
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(nullthrows(fragmentDefinitions.get(name)));
}
}
const referencedFragments = [];
for (const ast of asts) {
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._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();
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(nullthrows(objectTypeDefinitions.get(name)));
}
}
const referencedObjects = [];
for (const ast of asts) {
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;
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.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: 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 = extendSchema(schema, 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 === Kind.FRAGMENT_DEFINITION) {
fragmentDefinitions.set(definition.name.value, {
filePath,
content,
definition,
});
}
else if (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 readFile(URI.parse(filePath).fsPath, 'utf-8');
const asts = [];
let queries = [];
if (content.trim().length !== 0) {
try {
queries = await this._parser(content, filePath, DEFAULT_SUPPORTED_EXTENSIONS, 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(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 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: 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 === 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: 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 (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 Kind.OBJECT_TYPE_DEFINITION:
case Kind.INTERFACE_TYPE_DEFINITION:
case Kind.ENUM_TYPE_DEFINITION:
case Kind.UNION_TYPE_DEFINITION:
case Kind.SCALAR_TYPE_DEFINITION:
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
case Kind.SCALAR_TYPE_EXTENSION:
case Kind.OBJECT_TYPE_EXTENSION:
case Kind.INTERFACE_TYPE_EXTENSION:
case Kind.UNION_TYPE_EXTENSION:
case Kind.ENUM_TYPE_EXTENSION:
case Kind.INPUT_OBJECT_TYPE_EXTENSION:
case 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 = 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: 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';
}
}
//# sourceMappingURL=GraphQLCache.js.map