UNPKG

graphql-language-service-server

Version:
674 lines 29.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MessageProcessor = void 0; const mkdirp_1 = __importDefault(require("mkdirp")); const fs_1 = require("fs"); const url_1 = require("url"); const path = __importStar(require("path")); const graphql_language_service_1 = require("graphql-language-service"); const GraphQLCache_1 = require("./GraphQLCache"); const parseDocument_1 = require("./parseDocument"); const graphql_1 = require("graphql"); const os_1 = require("os"); const util_1 = require("util"); const writeFileAsync = util_1.promisify(fs_1.writeFile); require('dotenv').config(); function toPosition(position) { return new graphql_language_service_1.Position(position.line, position.character); } 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_1.parseDocument; return p(text, uri, fileExtensions, graphqlFileExtensions); }; this._tmpDir = tmpDir || os_1.tmpdir(); this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service'); this._tmpUriBase = url_1.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 (!fs_1.existsSync(this._tmpDirBase)) { mkdirp_1.default(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 GraphQLCache_1.getGraphQLCache({ parser: this._parser, loadConfigOptions: this._loadConfigOptions, config: this._graphQLConfig, }); this._languageService = new graphql_language_service_1.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 === graphql_language_service_1.FileChangeTypeKind.Created || change.type === graphql_language_service_1.FileChangeTypeKind.Changed) { const uri = change.uri; const text = fs_1.readFileSync(url_1.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 === graphql_language_service_1.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 = []; graphql_1.visit(graphql_1.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 = parseDocument_1.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 = fs_1.existsSync(uri); let version = 1; if (isFileUri) { const schemaUri = url_1.pathToFileURL(path.join(project.dirpath, uri)).toString(); const schemaDocument = this._getCachedDocument(schemaUri); if (schemaDocument) { version = schemaDocument.version++; } const schemaText = fs_1.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 (!fs_1.existsSync(projectTmpPath)) { mkdirp_1.default(projectTmpPath); } if (appendPath) { projectTmpPath = path.join(projectTmpPath, appendPath); } if (prependWithProtocol) { return url_1.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 = graphql_1.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) { fs_1.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 = url_1.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; } } exports.MessageProcessor = MessageProcessor; function processDiagnosticsMessage(results, query, range) { const queryLines = query.split('\n'); const totalLines = queryLines.length; const lastLineLength = queryLines[totalLines - 1].length; const lastCharacterPosition = new graphql_language_service_1.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 graphql_language_service_1.Range(new graphql_language_service_1.Position(diagnostic.range.start.line + offset.line, diagnostic.range.start.character), new graphql_language_service_1.Position(diagnostic.range.end.line + offset.line, diagnostic.range.end.character)), })); } return processedResults; } //# sourceMappingURL=MessageProcessor.js.map