graphql-language-service-server
Version:
Server process backing the GraphQL Language Service
648 lines • 27.6 kB
JavaScript
import mkdirp from 'mkdirp';
import { readFileSync, existsSync, writeFileSync, writeFile } from 'fs';
import { fileURLToPath, pathToFileURL } from 'url';
import * as path from 'path';
import { GraphQLLanguageService, FileChangeTypeKind, Range, Position, } from 'graphql-language-service';
import { getGraphQLCache } from './GraphQLCache';
import { parseDocument, DEFAULT_SUPPORTED_EXTENSIONS } from './parseDocument';
import { printSchema, visit, parse } from 'graphql';
import { tmpdir } from 'os';
import { promisify } from 'util';
const writeFileAsync = promisify(writeFile);
require('dotenv').config();
function toPosition(position) {
return new Position(position.line, position.character);
}
export class MessageProcessor {
constructor({ logger, fileExtensions, graphqlFileExtensions, loadConfigOptions, config, parser, tmpDir, connection, }) {
var _a;
this._schemaCacheInit = false;
this._rootPath = process.cwd();
this._connection = connection;
this._textDocumentCache = new Map();
this._isInitialized = false;
this._willShutdown = false;
this._logger = logger;
this._graphQLConfig = config;
this._parser = (text, uri) => {
const p = parser !== null && parser !== void 0 ? parser : parseDocument;
return p(text, uri, fileExtensions, graphqlFileExtensions);
};
this._tmpDir = tmpDir || tmpdir();
this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service');
this._tmpUriBase = pathToFileURL(this._tmpDirBase).toString();
this._loadConfigOptions = loadConfigOptions;
if (loadConfigOptions.extensions &&
((_a = loadConfigOptions.extensions) === null || _a === void 0 ? void 0 : _a.length) > 0) {
this._extensions = loadConfigOptions.extensions;
}
if (!existsSync(this._tmpDirBase)) {
mkdirp(this._tmpDirBase);
}
}
get connection() {
return this._connection;
}
set connection(connection) {
this._connection = connection;
}
async handleInitializeRequest(params, _token, configDir) {
if (!params) {
throw new Error('`params` argument is required to initialize.');
}
const serverCapabilities = {
capabilities: {
workspaceSymbolProvider: true,
documentSymbolProvider: true,
completionProvider: {
resolveProvider: true,
triggerCharacters: ['@'],
},
definitionProvider: true,
textDocumentSync: 1,
hoverProvider: true,
workspace: {
workspaceFolders: {
supported: true,
changeNotifications: true,
},
},
},
};
this._rootPath = configDir
? configDir.trim()
: params.rootUri || this._rootPath;
if (!this._rootPath) {
this._logger.warn('no rootPath configured in extension or server, defaulting to cwd');
}
if (!serverCapabilities) {
throw new Error('GraphQL Language Server is not initialized.');
}
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'initialize',
}));
return serverCapabilities;
}
async _updateGraphQLConfig() {
var _a, _b, _c;
const settings = await this._connection.workspace.getConfiguration({
section: 'graphql-config',
});
const vscodeSettings = await this._connection.workspace.getConfiguration({
section: 'vscode-graphql',
});
if (settings === null || settings === void 0 ? void 0 : settings.dotEnvPath) {
require('dotenv').config({ path: settings.dotEnvPath });
}
this._settings = { ...settings, ...vscodeSettings };
const rootDir = ((_b = (_a = this._settings) === null || _a === void 0 ? void 0 : _a.load) === null || _b === void 0 ? void 0 : _b.rootDir) || this._rootPath;
this._rootPath = rootDir;
this._loadConfigOptions = {
...Object.keys(this._settings.load || []).reduce((agg, key) => {
const value = this._settings.load[key];
if (value === undefined || value === null) {
delete agg[key];
}
return agg;
}, this._settings.load),
rootDir,
};
this._graphQLCache = await getGraphQLCache({
parser: this._parser,
loadConfigOptions: this._loadConfigOptions,
config: this._graphQLConfig,
});
this._languageService = new GraphQLLanguageService(this._graphQLCache);
if ((_c = this._graphQLCache) === null || _c === void 0 ? void 0 : _c.getGraphQLConfig) {
const config = this._graphQLCache.getGraphQLConfig();
await this._cacheAllProjectFiles(config);
}
}
async handleDidOpenOrSaveNotification(params) {
try {
if (!this._isInitialized || !this._graphQLCache) {
if (!this._settings) {
await this._updateGraphQLConfig();
this._isInitialized = true;
}
else {
return null;
}
}
}
catch (err) {
this._logger.error(err);
}
if (!params || !params.textDocument) {
throw new Error('`textDocument` argument is required.');
}
const { textDocument } = params;
const { uri } = textDocument;
const diagnostics = [];
let contents = [];
if ('text' in textDocument && textDocument.text) {
contents = this._parser(textDocument.text, uri);
await this._invalidateCache(textDocument, uri, contents);
}
else {
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (cachedDocument) {
contents = cachedDocument.contents;
}
}
if (this._isInitialized && this._graphQLCache) {
await Promise.all(contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(query, uri, this._isRelayCompatMode(query) ? false : true);
if (results && results.length > 0) {
diagnostics.push(...processDiagnosticsMessage(results, query, range));
}
}));
const project = this._graphQLCache.getProjectForFile(uri);
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'textDocument/didOpen',
projectName: project && project.name,
fileName: uri,
}));
}
return { uri, diagnostics };
}
async handleDidChangeNotification(params) {
if (!this._isInitialized || !this._graphQLCache) {
return null;
}
if (!params ||
!params.textDocument ||
!params.contentChanges ||
!params.textDocument.uri) {
throw new Error('`textDocument`, `textDocument.uri`, and `contentChanges` arguments are required.');
}
const textDocument = params.textDocument;
const contentChanges = params.contentChanges;
const contentChange = contentChanges[contentChanges.length - 1];
const uri = textDocument.uri;
const contents = this._parser(contentChange.text, uri);
await this._invalidateCache(textDocument, uri, contents);
const cachedDocument = this._getCachedDocument(uri);
if (!cachedDocument) {
return null;
}
await this._updateFragmentDefinition(uri, contents);
await this._updateObjectTypeDefinition(uri, contents);
const diagnostics = [];
await Promise.all(contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(query, uri);
if (results && results.length > 0) {
diagnostics.push(...processDiagnosticsMessage(results, query, range));
}
}));
const project = this._graphQLCache.getProjectForFile(uri);
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'textDocument/didChange',
projectName: project && project.name,
fileName: uri,
}));
return { uri, diagnostics };
}
async handleDidChangeConfiguration(_params) {
await this._updateGraphQLConfig();
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'workspace/didChangeConfiguration',
}));
return {};
}
handleDidCloseNotification(params) {
if (!this._isInitialized || !this._graphQLCache) {
return;
}
if (!params || !params.textDocument) {
throw new Error('`textDocument` is required.');
}
const textDocument = params.textDocument;
const uri = textDocument.uri;
if (this._textDocumentCache.has(uri)) {
this._textDocumentCache.delete(uri);
}
const project = this._graphQLCache.getProjectForFile(uri);
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'textDocument/didClose',
projectName: project && project.name,
fileName: uri,
}));
}
handleShutdownRequest() {
this._willShutdown = true;
return;
}
handleExitNotification() {
process.exit(this._willShutdown ? 0 : 1);
}
validateDocumentAndPosition(params) {
if (!params ||
!params.textDocument ||
!params.textDocument.uri ||
!params.position) {
throw new Error('`textDocument`, `textDocument.uri`, and `position` arguments are required.');
}
}
async handleCompletionRequest(params) {
if (!this._isInitialized || !this._graphQLCache) {
return [];
}
this.validateDocumentAndPosition(params);
const textDocument = params.textDocument;
const position = params.position;
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (!cachedDocument) {
throw new Error('A cached document cannot be found.');
}
const found = cachedDocument.contents.find(content => {
const currentRange = content.range;
if (currentRange && currentRange.containsPosition(toPosition(position))) {
return true;
}
});
if (!found) {
return [];
}
const { query, range } = found;
if (range) {
position.line -= range.start.line;
}
const result = await this._languageService.getAutocompleteSuggestions(query, toPosition(position), textDocument.uri);
const project = this._graphQLCache.getProjectForFile(textDocument.uri);
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'textDocument/completion',
projectName: project && project.name,
fileName: textDocument.uri,
}));
return { items: result, isIncomplete: false };
}
async handleHoverRequest(params) {
if (!this._isInitialized || !this._graphQLCache) {
return { contents: [] };
}
this.validateDocumentAndPosition(params);
const textDocument = params.textDocument;
const position = params.position;
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (!cachedDocument) {
throw new Error('A cached document cannot be found.');
}
const found = cachedDocument.contents.find(content => {
const currentRange = content.range;
if (currentRange && currentRange.containsPosition(toPosition(position))) {
return true;
}
});
if (!found) {
return { contents: [] };
}
const { query, range } = found;
if (range) {
position.line -= range.start.line;
}
const result = await this._languageService.getHoverInformation(query, toPosition(position), textDocument.uri);
return {
contents: result,
};
}
async handleWatchedFilesChangedNotification(params) {
if (!this._isInitialized || !this._graphQLCache) {
return null;
}
return Promise.all(params.changes.map(async (change) => {
if (!this._isInitialized || !this._graphQLCache) {
throw Error('No cache available for handleWatchedFilesChanged');
}
if (['graphql.config', 'graphqlrc', this._settings.load.fileName].some(v => { var _a; return (_a = change.uri.match(v)) === null || _a === void 0 ? void 0 : _a.length; })) {
this._logger.info('updating graphql config');
this._updateGraphQLConfig();
}
if (change.type === FileChangeTypeKind.Created ||
change.type === FileChangeTypeKind.Changed) {
const uri = change.uri;
const text = readFileSync(fileURLToPath(uri), { encoding: 'utf8' });
const contents = this._parser(text, uri);
await this._updateFragmentDefinition(uri, contents);
await this._updateObjectTypeDefinition(uri, contents);
const diagnostics = (await Promise.all(contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(query, uri);
if (results && results.length > 0) {
return processDiagnosticsMessage(results, query, range);
}
else {
return [];
}
}))).reduce((left, right) => left.concat(right), []);
const project = this._graphQLCache.getProjectForFile(uri);
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'workspace/didChangeWatchedFiles',
projectName: project && project.name,
fileName: uri,
}));
return { uri, diagnostics };
}
else if (change.type === FileChangeTypeKind.Deleted) {
this._graphQLCache.updateFragmentDefinitionCache(this._graphQLCache.getGraphQLConfig().dirpath, change.uri, false);
this._graphQLCache.updateObjectTypeDefinitionCache(this._graphQLCache.getGraphQLConfig().dirpath, change.uri, false);
}
}));
}
async handleDefinitionRequest(params, _token) {
if (!this._isInitialized || !this._graphQLCache) {
return [];
}
if (!params || !params.textDocument || !params.position) {
throw new Error('`textDocument` and `position` arguments are required.');
}
const textDocument = params.textDocument;
const position = params.position;
const project = this._graphQLCache.getProjectForFile(textDocument.uri);
if (project) {
await this._cacheSchemaFilesForProject(project);
}
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (!cachedDocument) {
return [];
}
const found = cachedDocument.contents.find(content => {
const currentRange = content.range;
if (currentRange && currentRange.containsPosition(toPosition(position))) {
return true;
}
});
if (!found) {
return [];
}
const { query, range: parentRange } = found;
if (parentRange) {
position.line -= parentRange.start.line;
}
let result = null;
try {
result = await this._languageService.getDefinition(query, toPosition(position), textDocument.uri);
}
catch (err) {
}
const inlineFragments = [];
visit(parse(query, {
allowLegacySDLEmptyFields: true,
allowLegacySDLImplementsInterfaces: true,
}), {
FragmentDefinition: (node) => {
inlineFragments.push(node.name.value);
},
});
const formatted = result
? result.definitions.map(res => {
const defRange = res.range;
if (parentRange && res.name) {
const isInline = inlineFragments.includes(res.name);
const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes(path.extname(textDocument.uri));
if (isInline && isEmbedded) {
const vOffset = parentRange.start.line;
defRange.setStart((defRange.start.line += vOffset), defRange.start.character);
defRange.setEnd((defRange.end.line += vOffset), defRange.end.character);
}
}
return {
uri: res.path,
range: defRange,
};
})
: [];
this._logger.log(JSON.stringify({
type: 'usage',
messageType: 'textDocument/definition',
projectName: project && project.name,
fileName: textDocument.uri,
}));
return formatted;
}
async handleDocumentSymbolRequest(params) {
if (!this._isInitialized) {
return [];
}
if (!params || !params.textDocument) {
throw new Error('`textDocument` argument is required.');
}
const textDocument = params.textDocument;
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (!cachedDocument || !cachedDocument.contents[0]) {
throw new Error('A cached document cannot be found.');
}
return this._languageService.getDocumentSymbols(cachedDocument.contents[0].query, textDocument.uri);
}
async handleWorkspaceSymbolRequest(params) {
if (params.query !== '') {
const documents = this._getTextDocuments();
const symbols = [];
await Promise.all(documents.map(async ([uri]) => {
const cachedDocument = this._getCachedDocument(uri);
if (!cachedDocument) {
return [];
}
const docSymbols = await this._languageService.getDocumentSymbols(cachedDocument.contents[0].query, uri);
symbols.push(...docSymbols);
}));
return symbols.filter(symbol => (symbol === null || symbol === void 0 ? void 0 : symbol.name) && symbol.name.includes(params.query));
}
return [];
}
_getTextDocuments() {
return Array.from(this._textDocumentCache);
}
async _cacheSchemaText(uri, text, version) {
try {
const contents = this._parser(text, uri);
if (contents.length > 0) {
await this._invalidateCache({ version, uri }, uri, contents);
await this._updateObjectTypeDefinition(uri, contents);
}
}
catch (err) {
this._logger.error(err);
}
}
async _cacheSchemaFile(uri, project) {
uri = uri.toString();
const isFileUri = existsSync(uri);
let version = 1;
if (isFileUri) {
const schemaUri = pathToFileURL(path.join(project.dirpath, uri)).toString();
const schemaDocument = this._getCachedDocument(schemaUri);
if (schemaDocument) {
version = schemaDocument.version++;
}
const schemaText = readFileSync(uri, { encoding: 'utf-8' });
this._cacheSchemaText(schemaUri, schemaText, version);
}
}
_getTmpProjectPath(project, prependWithProtocol = true, appendPath) {
const baseDir = this._graphQLCache.getGraphQLConfig().dirpath;
const workspaceName = path.basename(baseDir);
const basePath = path.join(this._tmpDirBase, workspaceName);
let projectTmpPath = path.join(basePath, 'projects', project.name);
if (!existsSync(projectTmpPath)) {
mkdirp(projectTmpPath);
}
if (appendPath) {
projectTmpPath = path.join(projectTmpPath, appendPath);
}
if (prependWithProtocol) {
return pathToFileURL(path.resolve(projectTmpPath)).toString();
}
else {
return path.resolve(projectTmpPath);
}
}
async _cacheSchemaFilesForProject(project) {
var _a, _b, _c;
const schema = project === null || project === void 0 ? void 0 : project.schema;
const config = (_a = project === null || project === void 0 ? void 0 : project.extensions) === null || _a === void 0 ? void 0 : _a.languageService;
const useSchemaFileDefinitions = ((_b = config === null || config === void 0 ? void 0 : config.useSchemaFileDefinitions) !== null && _b !== void 0 ? _b : (_c = this === null || this === void 0 ? void 0 : this._settings) === null || _c === void 0 ? void 0 : _c.useSchemaFileDefinitions) ||
false;
if (!useSchemaFileDefinitions) {
await this._cacheConfigSchema(project);
}
else {
if (Array.isArray(schema)) {
Promise.all(schema.map(async (uri) => {
await this._cacheSchemaFile(uri, project);
}));
}
else {
const uri = schema.toString();
await this._cacheSchemaFile(uri, project);
}
}
}
async _cacheConfigSchema(project) {
try {
const schema = await this._graphQLCache.getSchema(project.name);
if (schema) {
let schemaText = printSchema(schema, {
commentDescriptions: true,
});
const uri = this._getTmpProjectPath(project, true, 'generated-schema.graphql');
const fsPath = this._getTmpProjectPath(project, false, 'generated-schema.graphql');
schemaText = `# This is an automatically generated representation of your schema.\n# Any changes to this file will be overwritten and will not be\n# reflected in the resulting GraphQL schema\n\n${schemaText}`;
const cachedSchemaDoc = this._getCachedDocument(uri);
if (!cachedSchemaDoc) {
await writeFileAsync(fsPath, schemaText, {
encoding: 'utf-8',
});
await this._cacheSchemaText(uri, schemaText, 1);
}
if (cachedSchemaDoc) {
writeFileSync(fsPath, schemaText, {
encoding: 'utf-8',
});
await this._cacheSchemaText(uri, schemaText, cachedSchemaDoc.version++);
}
}
}
catch (err) {
this._logger.error(err);
}
}
async _cacheDocumentFilesforProject(project) {
try {
const documents = await project.getDocuments();
return Promise.all(documents.map(async (document) => {
if (!document.location || !document.rawSDL) {
return;
}
let filePath = document.location;
if (!path.isAbsolute(filePath)) {
filePath = path.join(project.dirpath, document.location);
}
const uri = pathToFileURL(filePath).toString();
const contents = this._parser(document.rawSDL, uri);
if (!contents[0] || !contents[0].query) {
return;
}
await this._updateObjectTypeDefinition(uri, contents);
await this._updateFragmentDefinition(uri, contents);
await this._invalidateCache({ version: 1, uri }, uri, contents);
}));
}
catch (err) {
this._logger.error(`invalid/unknown file in graphql config documents entry:\n '${project.documents}'`);
this._logger.error(err);
}
}
async _cacheAllProjectFiles(config) {
if (config === null || config === void 0 ? void 0 : config.projects) {
return Promise.all(Object.keys(config.projects).map(async (projectName) => {
const project = await config.getProject(projectName);
await this._cacheSchemaFilesForProject(project);
await this._cacheDocumentFilesforProject(project);
}));
}
}
_isRelayCompatMode(query) {
return (query.indexOf('RelayCompat') !== -1 ||
query.indexOf('react-relay/compat') !== -1);
}
async _updateFragmentDefinition(uri, contents) {
const rootDir = this._graphQLCache.getGraphQLConfig().dirpath;
await this._graphQLCache.updateFragmentDefinition(rootDir, uri, contents);
}
async _updateObjectTypeDefinition(uri, contents) {
const rootDir = this._graphQLCache.getGraphQLConfig().dirpath;
await this._graphQLCache.updateObjectTypeDefinition(rootDir, uri, contents);
}
_getCachedDocument(uri) {
if (this._textDocumentCache.has(uri)) {
const cachedDocument = this._textDocumentCache.get(uri);
if (cachedDocument) {
return cachedDocument;
}
}
return null;
}
async _invalidateCache(textDocument, uri, contents) {
if (this._textDocumentCache.has(uri)) {
const cachedDocument = this._textDocumentCache.get(uri);
if (cachedDocument &&
textDocument && (textDocument === null || textDocument === void 0 ? void 0 : textDocument.version) &&
cachedDocument.version < textDocument.version) {
return this._textDocumentCache.set(uri, {
version: textDocument.version,
contents,
});
}
}
else if (textDocument === null || textDocument === void 0 ? void 0 : textDocument.version) {
return this._textDocumentCache.set(uri, {
version: textDocument.version,
contents,
});
}
return null;
}
}
function processDiagnosticsMessage(results, query, range) {
const queryLines = query.split('\n');
const totalLines = queryLines.length;
const lastLineLength = queryLines[totalLines - 1].length;
const lastCharacterPosition = new Position(totalLines, lastLineLength);
const processedResults = results.filter(diagnostic => diagnostic.range.end.lessThanOrEqualTo(lastCharacterPosition));
if (range) {
const offset = range.start;
return processedResults.map(diagnostic => ({
...diagnostic,
range: new Range(new Position(diagnostic.range.start.line + offset.line, diagnostic.range.start.character), new Position(diagnostic.range.end.line + offset.line, diagnostic.range.end.character)),
}));
}
return processedResults;
}
//# sourceMappingURL=MessageProcessor.js.map