From 8e33dbba168b23c3ea3e527a8c50ecffd7c57167 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 18 Jan 2020 03:40:36 -0500 Subject: [PATCH] feat: convert LSP Server to Typescript, remove watchman (#1138) --- .eslintignore | 2 +- .eslintrc.js | 13 +- .flowconfig | 3 + jest.config.js | 1 + package.json | 3 +- packages/codemirror-graphql/package.json | 2 +- .../new-components/Toolbar/Tabs.stories.js | 2 +- .../new-components/Toolbar/Toolbar.stories.js | 2 +- .../src/new-components/Type.stories.js | 2 +- .../new-components/themes}/decorators.js | 0 packages/graphiql/tsconfig.json | 2 +- .../src/GraphQLLanguageService.ts | 15 +- .../src/autocompleteUtils.ts | 3 +- .../src/getAutocompleteSuggestions.ts | 22 +- .../src/getDefinition.ts | 16 +- .../src/getDiagnostics.ts | 19 +- .../package.json | 7 +- .../src/{GraphQLCache.js => GraphQLCache.ts} | 229 ++++++++---------- .../src/GraphQLWatchman.js | 125 ---------- .../src/{Logger.js => Logger.ts} | 11 +- ...essageProcessor.js => MessageProcessor.ts} | 202 ++++++--------- ...phQLCache-test.js => GraphQLCache-test.ts} | 83 +------ ...essor-test.js => MessageProcessor-test.ts} | 114 ++++----- .../src/findGraphQLTags.js | 195 --------------- .../src/findGraphQLTags.ts | 219 +++++++++++++++++ .../src/{index.js => index.ts} | 3 - .../src/{startServer.js => startServer.ts} | 27 ++- .../src/{stringToHash.js => stringToHash.ts} | 3 +- .../tsconfig.esm.json | 19 ++ .../tsconfig.json | 20 ++ .../src/index.ts | 94 +++---- .../tsconfig.json | 1 + .../src/file.ts | 9 + .../tsconfig.esm.json | 3 +- .../tsconfig.json | 3 +- packages/graphql-language-service/README.md | 13 +- .../graphql-language-service/package.json | 2 +- .../{index-test.js => index-test.ts} | 0 .../src/{cli.js => cli.ts} | 23 +- .../src/{client.js => client.ts} | 46 ++-- .../tsconfig.esm.json | 23 ++ .../graphql-language-service/tsconfig.json | 23 ++ resources/tsconfig.build.cjs.json | 6 + resources/tsconfig.build.esm.json | 6 + resources/util.js | 1 - yarn.lock | 5 + 46 files changed, 733 insertions(+), 889 deletions(-) rename packages/graphiql/{.storybook => src/new-components/themes}/decorators.js (100%) rename packages/graphql-language-service-server/src/{GraphQLCache.js => GraphQLCache.ts} (82%) delete mode 100644 packages/graphql-language-service-server/src/GraphQLWatchman.js rename packages/graphql-language-service-server/src/{Logger.js => Logger.ts} (88%) rename packages/graphql-language-service-server/src/{MessageProcessor.js => MessageProcessor.ts} (79%) rename packages/graphql-language-service-server/src/__tests__/{GraphQLCache-test.js => GraphQLCache-test.ts} (75%) rename packages/graphql-language-service-server/src/__tests__/{MessageProcessor-test.js => MessageProcessor-test.ts} (76%) delete mode 100644 packages/graphql-language-service-server/src/findGraphQLTags.js create mode 100644 packages/graphql-language-service-server/src/findGraphQLTags.ts rename packages/graphql-language-service-server/src/{index.js => index.ts} (88%) rename packages/graphql-language-service-server/src/{startServer.js => startServer.ts} (89%) rename packages/graphql-language-service-server/src/{stringToHash.js => stringToHash.ts} (88%) create mode 100644 packages/graphql-language-service-server/tsconfig.esm.json create mode 100644 packages/graphql-language-service-server/tsconfig.json rename packages/graphql-language-service/src/__tests__/{index-test.js => index-test.ts} (100%) rename packages/graphql-language-service/src/{cli.js => cli.ts} (90%) rename packages/graphql-language-service/src/{client.js => client.ts} (81%) create mode 100644 packages/graphql-language-service/tsconfig.esm.json create mode 100644 packages/graphql-language-service/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 7886761fd48..75407c0c2c6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -33,7 +33,7 @@ packages/graphiql/storybook packages/graphiql/lsp **/renderExample.js **/*.min.js -/coverage/ +**/coverage/ # codemirror's build artefacts are exported from the package root diff --git a/.eslintrc.js b/.eslintrc.js index e6a5abed8be..1ac57eff510 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,7 +9,7 @@ module.exports = { root: true, - parser: 'babel-eslint', + parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 7, sourceType: 'module', @@ -273,7 +273,7 @@ module.exports = { 'prefer-object-spread/prefer-object-spread': 1, }, - plugins: ['babel', 'import', 'flowtype', 'prefer-object-spread'], + plugins: ['import', 'prefer-object-spread'], overrides: [ // Cypress plugin, global, etc only for cypress directory @@ -302,18 +302,15 @@ module.exports = { // Rules for TypeScript only { files: ['*.ts', '*.tsx'], - parser: '@typescript-eslint/parser', rules: { 'no-unused-vars': 'off', }, }, // Rules for Flow only { - files: [ - 'packages/codemirror-graphql/src/**/*.js', - 'packages/codemirror-graphql/src/**/*.jsx', - ], - plugins: ['flowtype'], + files: ['packages/codemirror-graphql/src/**/*.js'], + parser: 'babel-eslint', + plugins: ['flowtype', 'babel'], rules: { // flowtype (https://github.com/gajus/eslint-plugin-flowtype) 'flowtype/boolean-style': 1, diff --git a/.flowconfig b/.flowconfig index 519590aa719..581baffed5e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,7 +1,10 @@ [ignore] .*/__mocks__/.* .*/__tests__/.* +!*.flow.js .*/coverage/.* +!packages/codemirror-graphql/.* +!packages/codemirror-graphql/.* .*/dist/.* .*/resources/.* .*/node_modules/.* diff --git a/jest.config.js b/jest.config.js index d48017968cf..bb3752dc4db 100644 --- a/jest.config.js +++ b/jest.config.js @@ -39,5 +39,6 @@ module.exports = { '!**/resources/**', '!**/examples/**', '!**/codemirror-graphql/**', + '!**/graphql-language-service-types/**', ], }; diff --git a/package.json b/package.json index 78c57cb4c1e..4a84f95b572 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "scripts": { "build": "yarn run build-clean && yarn build-ts && yarn build-js", - "build-js": "lerna run build --scope codemirror-graphql --scope graphql-language-service-server --scope graphql-language-service", + "build-js": "lerna run build --scope codemirror-graphql", "build-ts": "yarn run tsc", "build-clean": "yarn run tsc --clean && rimraf '{packages,examples}/**/{dist,esm,bundle,cdn,webpack,storybook}' && lerna run build-clean --parallel", "build-validate": "lerna run build-validate", @@ -63,6 +63,7 @@ "@commitlint/config-conventional": "^8.1.0", "@commitlint/config-lerna-scopes": "^8.1.0", "@strictsoftware/typedoc-plugin-monorepo": "^0.2.1", + "@types/fetch-mock": "^7.3.2", "@types/jest": "^24.0.18", "@typescript-eslint/parser": "^2.3.0", "babel-eslint": "^10.0.1", diff --git a/packages/codemirror-graphql/package.json b/packages/codemirror-graphql/package.json index ac3e06b720c..d308d32480e 100644 --- a/packages/codemirror-graphql/package.json +++ b/packages/codemirror-graphql/package.json @@ -45,7 +45,7 @@ "build-clean": "rimraf {mode,hint,info,jump,lint}.{js,esm.js,js.flow} && rimraf esm results utils variables coverage __tests__", "build-flow": "node ../../resources/buildFlow.js", "watch": "babel --optional runtime resources/watch.js | node", - "test": "nyc --require @babel/polyfill --reporter lcov mocha $npm_package_options_mocha" + "test": "nyc --require @babel/polyfill --reporter lcov --reporter text mocha $npm_package_options_mocha" }, "peerDependencies": { "codemirror": "^5.26.0", diff --git a/packages/graphiql/src/new-components/Toolbar/Tabs.stories.js b/packages/graphiql/src/new-components/Toolbar/Tabs.stories.js index e9a85cd8935..22bc90701c9 100644 --- a/packages/graphiql/src/new-components/Toolbar/Tabs.stories.js +++ b/packages/graphiql/src/new-components/Toolbar/Tabs.stories.js @@ -1,7 +1,7 @@ import List, { ListRow } from '../List/List'; import Tabs from './Tabs'; import React, { useState } from 'react'; -import { layout } from '../../../.storybook/decorators'; +import { layout } from '../themes/decorators'; export default { title: 'Tabbar', decorators: [layout] }; diff --git a/packages/graphiql/src/new-components/Toolbar/Toolbar.stories.js b/packages/graphiql/src/new-components/Toolbar/Toolbar.stories.js index 721c90c479e..d20bf1d1bcf 100644 --- a/packages/graphiql/src/new-components/Toolbar/Toolbar.stories.js +++ b/packages/graphiql/src/new-components/Toolbar/Toolbar.stories.js @@ -3,7 +3,7 @@ import Tabs from './Tabs'; import React from 'react'; import Toolbar from './Toolbar'; import Content from './Content'; -import { layout } from '../../../.storybook/decorators'; +import { layout } from '../themes/decorators'; export default { title: 'Toolbar', decorators: [layout] }; diff --git a/packages/graphiql/src/new-components/Type.stories.js b/packages/graphiql/src/new-components/Type.stories.js index 3f7d6b0f333..3c52f88dead 100644 --- a/packages/graphiql/src/new-components/Type.stories.js +++ b/packages/graphiql/src/new-components/Type.stories.js @@ -2,7 +2,7 @@ import { jsx } from 'theme-ui'; import List, { ListRow } from './List/List'; import { SectionHeader, Explainer } from './Type'; -import { layout } from '../../.storybook/decorators'; +import { layout } from './themes/decorators'; export default { title: 'Type', decorators: [layout] }; diff --git a/packages/graphiql/.storybook/decorators.js b/packages/graphiql/src/new-components/themes/decorators.js similarity index 100% rename from packages/graphiql/.storybook/decorators.js rename to packages/graphiql/src/new-components/themes/decorators.js diff --git a/packages/graphiql/tsconfig.json b/packages/graphiql/tsconfig.json index 2dbb6d9ccb9..28e2fa12a97 100644 --- a/packages/graphiql/tsconfig.json +++ b/packages/graphiql/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", - "composite": false, + "composite": true, "jsx": "react", "allowJs": true, "baseUrl": ".", diff --git a/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts b/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts index 94bb76640d9..6e1ec96f3b1 100644 --- a/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-interface/src/GraphQLLanguageService.ts @@ -25,10 +25,11 @@ import { GraphQLProjectConfig, Uri, Position, + CustomValidationRule, } from 'graphql-language-service-types'; // import { Position } from 'graphql-language-service-utils'; -import { Hover } from 'vscode-languageserver-types'; +import { Hover, DiagnosticSeverity } from 'vscode-languageserver-types'; import { Kind, parse, print } from 'graphql'; import { getAutocompleteSuggestions } from './getAutocompleteSuggestions'; @@ -122,7 +123,7 @@ export class GraphQLLanguageService { const range = getRange(error.locations[0], query); return [ { - severity: SEVERITY.ERROR, + severity: SEVERITY.ERROR as DiagnosticSeverity, message: error.message, source: 'GraphQL: Syntax', range, @@ -166,7 +167,9 @@ export class GraphQLLanguageService { /* eslint-disable no-implicit-coercion */ const rulesPath = resolveFile(customRulesModulePath); if (rulesPath) { - const customValidationRules = await requireFile(rulesPath); + const customValidationRules: ( + config: GraphQLConfig, + ) => CustomValidationRule[] = await requireFile(rulesPath); if (customValidationRules) { customRules = customValidationRules(this._graphQLConfig); } @@ -220,7 +223,7 @@ export class GraphQLLanguageService { query: string, position: Position, filePath: Uri, - ): Promise { + ): Promise { const projectConfig = this.getConfigForURI(filePath); let ast; @@ -269,7 +272,7 @@ export class GraphQLLanguageService { node: NamedTypeNode, filePath: Uri, projectConfig: GraphQLProjectConfig, - ): Promise { + ): Promise { const objectTypeDefinitions = await this._graphQLCache.getObjectTypeDefinitions( projectConfig, ); @@ -313,7 +316,7 @@ export class GraphQLLanguageService { node: FragmentSpreadNode, filePath: Uri, projectConfig: GraphQLProjectConfig, - ): Promise { + ): Promise { const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( projectConfig, ); diff --git a/packages/graphql-language-service-interface/src/autocompleteUtils.ts b/packages/graphql-language-service-interface/src/autocompleteUtils.ts index 198dce43b0b..fe670dccb3d 100644 --- a/packages/graphql-language-service-interface/src/autocompleteUtils.ts +++ b/packages/graphql-language-service-interface/src/autocompleteUtils.ts @@ -29,8 +29,7 @@ export function getDefinitionState( let definitionState; // TODO - couldn't figure this one out - // @ts-ignore - forEachState(tokenState, (state: State): AllTypeInfo | null | undefined => { + forEachState(tokenState, (state: State): void => { switch (state.kind) { case 'Query': case 'ShortQuery': diff --git a/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts b/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts index 733e5aeedae..daf2823ae6d 100644 --- a/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts +++ b/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts @@ -191,6 +191,7 @@ function getSuggestionsForFieldNames( label: field.name, detail: String(field.type), documentation: field.description, + deprecated: field.isDeprecated, isDeprecated: field.isDeprecated, deprecationReason: field.deprecationReason, })), @@ -202,19 +203,22 @@ function getSuggestionsForFieldNames( function getSuggestionsForInputValues( token: ContextToken, typeInfo: AllTypeInfo, -): Array { +): CompletionItem[] { const namedInputType = getNamedType(typeInfo.inputType as GraphQLType); if (namedInputType instanceof GraphQLEnumType) { - const values = namedInputType.getValues(); + const values: GraphQLEnumValues[] = namedInputType.getValues(); return hintList( token, - values.map(value => ({ - label: value.name, - detail: String(namedInputType), - documentation: value.description, - isDeprecated: value.isDeprecated, - deprecationReason: value.deprecationReason, - })), + values.map( + (value: GraphQLEnumValue): CompletionItem => ({ + label: value.name, + detail: String(namedInputType), + documentation: value.description, + deprecated: value.isDeprecated, + isDeprecated: value.isDeprecated, + deprecationReason: value.deprecationReason, + }), + ), ); } else if (namedInputType === GraphQLBoolean) { return hintList(token, [ diff --git a/packages/graphql-language-service-interface/src/getDefinition.ts b/packages/graphql-language-service-interface/src/getDefinition.ts index 32edfc0ed35..0ad49b0337d 100644 --- a/packages/graphql-language-service-interface/src/getDefinition.ts +++ b/packages/graphql-language-service-interface/src/getDefinition.ts @@ -28,19 +28,19 @@ import { } from 'graphql-language-service-types'; import { locToRange, offsetToPosition } from 'graphql-language-service-utils'; -import invariant from 'assert'; +import assert from 'assert'; export const LANGUAGE = 'GraphQL'; function getRange(text: string, node: ASTNode): Range { const location = node.loc as Location; - invariant(location, 'Expected ASTNode to have a location.'); + assert(location, 'Expected ASTNode to have a location.'); return locToRange(text, location); } function getPosition(text: string, node: ASTNode): Position { const location = node.loc as Location; - invariant(location, 'Expected ASTNode to have a location.'); + assert(location, 'Expected ASTNode to have a location.'); return offsetToPosition(text, location.start); } @@ -111,14 +111,16 @@ function getDefinitionForFragmentDefinition( definition: FragmentDefinitionNode | OperationDefinitionNode, ): Definition { const name = definition.name; - invariant(name, 'Expected ASTNode to have a Name.'); + if (!name) { + throw Error('Expected ASTNode to have a Name.'); + } + return { path, position: getPosition(text, definition), range: getRange(text, definition), - // @ts-ignore // TODO: doesnt seem to pick up the inference - // from invariant() exception logic + // from assert() exception logic name: name.value || '', language: LANGUAGE, // This is a file inside the project root, good enough for now @@ -132,7 +134,7 @@ function getDefinitionForNodeDefinition( definition: TypeDefinitionNode, ): Definition { const name = definition.name; - invariant(name, 'Expected ASTNode to have a Name.'); + assert(name, 'Expected ASTNode to have a Name.'); return { path, position: getPosition(text, definition), diff --git a/packages/graphql-language-service-interface/src/getDiagnostics.ts b/packages/graphql-language-service-interface/src/getDiagnostics.ts index 3d333439fe5..68c07afe095 100644 --- a/packages/graphql-language-service-interface/src/getDiagnostics.ts +++ b/packages/graphql-language-service-interface/src/getDiagnostics.ts @@ -16,10 +16,7 @@ import { SourceLocation, } from 'graphql'; -import { - Diagnostic, - CustomValidationRule, -} from 'graphql-language-service-types'; +import { CustomValidationRule } from 'graphql-language-service-types'; import invariant from 'assert'; import { findDeprecatedUsages, parse } from 'graphql'; @@ -32,11 +29,13 @@ import { Position, } from 'graphql-language-service-utils'; +import { DiagnosticSeverity, Diagnostic } from 'vscode-languageserver-types'; + export const SEVERITY = { - ERROR: 1, - WARNING: 2, - INFORMATION: 3, - HINT: 4, + ERROR: 1 as DiagnosticSeverity, + WARNING: 2 as DiagnosticSeverity, + INFORMATION: 3 as DiagnosticSeverity, + HINT: 4 as DiagnosticSeverity, }; export function getDiagnostics( @@ -52,7 +51,7 @@ export function getDiagnostics( const range = getRange(error.locations[0], query); return [ { - severity: SEVERITY.ERROR, + severity: SEVERITY.ERROR as DiagnosticSeverity, message: error.message, source: 'GraphQL: Syntax', range, @@ -100,7 +99,7 @@ function mapCat( function annotations( error: GraphQLError, - severity: number, + severity: DiagnosticSeverity, type: string, ): Array { if (!error.nodes) { diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json index f0ec384c598..b06dc6ec548 100644 --- a/packages/graphql-language-service-server/package.json +++ b/packages/graphql-language-service-server/package.json @@ -25,9 +25,9 @@ "typings": "esm/index.d.ts", "scripts": { "test": "node ../../resources/runTests.js", - "build": "yarn build-js && yarn build-esm && yarn build-flow", - "build-js": "node ../../resources/buildJs.js", - "build-esm": "cross-env ESM=true babel src --root-mode upward --ignore **/__tests__/**,**/__mocks__/** --out-dir esm", + "build": "yarn build-js && yarn build-flow", + "build-js": "rimraf dist && tsc", + "build-esm": "rimraf esm && tsc -p tsconfig.esm", "build-flow": "node ../../resources/buildFlow.js" }, "peerDependencies": { @@ -35,7 +35,6 @@ }, "dependencies": { "@babel/parser": "^7.4.5", - "fb-watchman": "^2.0.0", "glob": "^7.1.2", "graphql-config": "2.2.1", "graphql-language-service-interface": "^2.3.3", diff --git a/packages/graphql-language-service-server/src/GraphQLCache.js b/packages/graphql-language-service-server/src/GraphQLCache.ts similarity index 82% rename from packages/graphql-language-service-server/src/GraphQLCache.js rename to packages/graphql-language-service-server/src/GraphQLCache.ts index d476b21afc7..748cb1fc8e7 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.js +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -5,11 +5,10 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type { ASTNode, DocumentNode } from 'graphql/language'; -import type { +import { ASTNode, DocumentNode, DefinitionNode } from 'graphql/language'; +import { CachedContent, GraphQLCache as GraphQLCacheInterface, GraphQLConfig as GraphQLConfigInterface, @@ -22,7 +21,6 @@ import type { } from 'graphql-language-service-types'; import fs from 'fs'; -import path from 'path'; import { GraphQLSchema, Kind, extendSchema, parse, visit } from 'graphql'; import nullthrows from 'nullthrows'; @@ -56,9 +54,7 @@ const { DIRECTIVE_DEFINITION, } = Kind; -export async function getGraphQLCache( - configDir: Uri, -): Promise { +export async function getGraphQLCache(configDir: Uri): Promise { const graphQLConfig = await getGraphQLConfig(configDir); return new GraphQLCache(configDir, graphQLConfig); } @@ -67,13 +63,12 @@ export class GraphQLCache implements GraphQLCacheInterface { _configDir: Uri; _graphQLFileListCache: Map>; _graphQLConfig: GraphQLConfig; - _cachePromise: Promise; _schemaMap: Map; _typeExtensionMap: Map; _fragmentDefinitionsCache: Map>; _typeDefinitionsCache: Map>; - constructor(configDir: Uri, graphQLConfig: GraphQLConfig): void { + constructor(configDir: Uri, graphQLConfig: GraphQLConfig) { this._configDir = configDir; this._graphQLConfig = graphQLConfig; this._graphQLFileListCache = new Map(); @@ -87,8 +82,8 @@ export class GraphQLCache implements GraphQLCacheInterface { getFragmentDependencies = async ( query: string, - fragmentDefinitions: ?Map, - ): Promise> => { + fragmentDefinitions?: Map | null, + ): Promise => { // If there isn't context for fragment references, // return an empty array. if (!fragmentDefinitions) { @@ -111,13 +106,13 @@ export class GraphQLCache implements GraphQLCacheInterface { getFragmentDependenciesForAST = async ( parsedQuery: ASTNode, fragmentDefinitions: Map, - ): Promise> => { + ): Promise => { if (!fragmentDefinitions) { return []; } const existingFrags = new Map(); - const referencedFragNames = new Set(); + const referencedFragNames = new Set(); visit(parsedQuery, { FragmentDefinition(node) { @@ -130,14 +125,14 @@ export class GraphQLCache implements GraphQLCacheInterface { }, }); - const asts = new Set(); + const asts = new Set(); referencedFragNames.forEach(name => { if (!existingFrags.has(name) && fragmentDefinitions.has(name)) { asts.add(nullthrows(fragmentDefinitions.get(name))); } }); - const referencedFragments = []; + const referencedFragments: FragmentInfo[] = []; asts.forEach(ast => { visit(ast.definition, { @@ -190,7 +185,7 @@ export class GraphQLCache implements GraphQLCacheInterface { getObjectTypeDependencies = async ( query: string, - objectTypeDefinitions: ?Map, + objectTypeDefinitions?: Map, ): Promise> => { // If there isn't context for object type references, // return an empty array. @@ -223,7 +218,7 @@ export class GraphQLCache implements GraphQLCacheInterface { } const existingObjectTypes = new Map(); - const referencedObjectTypes = new Set(); + const referencedObjectTypes = new Set(); visit(parsedQuery, { ObjectTypeDefinition(node) { @@ -242,14 +237,14 @@ export class GraphQLCache implements GraphQLCacheInterface { }, }); - const asts = new Set(); + const asts = new Set(); referencedObjectTypes.forEach(name => { if (!existingObjectTypes.has(name) && objectTypeDefinitions.has(name)) { asts.add(nullthrows(objectTypeDefinitions.get(name))); } }); - const referencedObjects = []; + const referencedObjects: ObjectTypeInfo[] = []; asts.forEach(ast => { visit(ast.definition, { @@ -297,72 +292,6 @@ export class GraphQLCache implements GraphQLCacheInterface { return objectTypeDefinitions; }; - handleWatchmanSubscribeEvent = ( - rootDir: string, - projectConfig: GraphQLProjectConfig, - ) => (result: Object) => { - if (result.files && result.files.length > 0) { - const graphQLFileMap = this._graphQLFileListCache.get(rootDir); - result.files.forEach(async ({ name, exists, size, mtime }) => { - const filePath = path.join(result.root, result.subscription, name); - if (projectConfig.schemaPath && filePath === projectConfig.schemaPath) { - this._invalidateSchemaCacheForProject(projectConfig); - } - - if (!graphQLFileMap) { - return; - } - // Prune the file using the input/excluded directories - if (!projectConfig.includesFile(name)) { - return; - } - - // In the event of watchman recrawl (is_fresh_instance), - // watchman subscription returns a full set of files within the - // watched directory. After pruning with input/excluded directories, - // the file could have been created/modified. - // Using the cached size/mtime information, only cache the file if - // the file doesn't exist or the file exists and one of or both - // size/mtime is different. - if (result.is_fresh_instance && exists) { - const existingFile = graphQLFileMap.get(filePath); - // Same size/mtime means the file stayed the same - if ( - existingFile && - existingFile.size === size && - existingFile.mtime === mtime - ) { - return; - } - - const fileAndContent = await this.promiseToReadGraphQLFile(filePath); - graphQLFileMap.set(filePath, { - ...fileAndContent, - size, - mtime, - }); - // Otherwise, create/update the cache with the updated file and - // content, or delete the cache if (!exists) - } else { - if (graphQLFileMap) { - this._graphQLFileListCache.set( - rootDir, - await this._updateGraphQLFileListCache( - graphQLFileMap, - { size, mtime }, - filePath, - exists, - ), - ); - } - - this.updateFragmentDefinitionCache(rootDir, filePath, exists); - this.updateObjectTypeDefinitionCache(rootDir, filePath, exists); - } - }); - } - }; - _readFilesFromInputDirs = ( rootDir: string, includes: string[], @@ -399,7 +328,7 @@ export class GraphQLCache implements GraphQLCacheInterface { '**/__flowtests__/**', ], }, - (error, results) => { + error => { if (error) { reject(error); } @@ -411,13 +340,20 @@ export class GraphQLCache implements GraphQLCacheInterface { .filter( filePath => typeof globResult.statCache[filePath] === 'object', ) - .map(filePath => ({ - filePath, - mtime: Math.trunc( - globResult.statCache[filePath].mtime.getTime() / 1000, - ), - size: globResult.statCache[filePath].size, - })), + .map(filePath => { + // @TODO + // so we have to force this here + // becase glob's DefinatelyTyped doesn't use fs.Stats here though + // the docs indicate that is what's there :shrug: + const cacheEntry: fs.Stats = globResult.statCache[ + filePath + ] as fs.Stats; + return { + filePath, + mtime: Math.trunc(cacheEntry.mtime.getTime() / 1000), + size: cacheEntry.size, + }; + }), ); }); }); @@ -425,7 +361,7 @@ export class GraphQLCache implements GraphQLCacheInterface { async _updateGraphQLFileListCache( graphQLFileMap: Map, - metrics: { size: number, mtime: number }, + metrics: { size: number; mtime: number }, filePath: Uri, exists: boolean, ): Promise> { @@ -589,11 +525,11 @@ export class GraphQLCache implements GraphQLCacheInterface { _extendSchema( schema: GraphQLSchema, - schemaPath: ?string, - schemaCacheKey: ?string, + schemaPath: string | null, + schemaCacheKey: string | null, ): GraphQLSchema { const graphQLFileMap = this._graphQLFileListCache.get(this._configDir); - const typeExtensions = []; + const typeExtensions: DefinitionNode[] = []; if (!graphQLFileMap) { return schema; @@ -650,9 +586,9 @@ export class GraphQLCache implements GraphQLCacheInterface { } getSchema = async ( - appName: ?string, - queryHasExtensions?: ?boolean = false, - ): Promise => { + appName?: string, + queryHasExtensions?: boolean | null, + ): Promise => { const projectConfig = this._graphQLConfig.getProjectConfig(appName); if (!projectConfig) { @@ -670,12 +606,12 @@ export class GraphQLCache implements GraphQLCacheInterface { if (endpointInfo && endpointKey) { const { endpoint } = endpointInfo; - schemaCacheKey = endpointKey; // Maybe use cache if (this._schemaMap.has(schemaCacheKey)) { schema = this._schemaMap.get(schemaCacheKey); + // @ts-ignore return schema && queryHasExtensions ? this._extendSchema(schema, schemaPath, schemaCacheKey) : schema; @@ -695,9 +631,11 @@ export class GraphQLCache implements GraphQLCacheInterface { // Maybe use cache if (this._schemaMap.has(schemaCacheKey)) { schema = this._schemaMap.get(schemaCacheKey); - return schema && queryHasExtensions - ? this._extendSchema(schema, schemaPath, schemaCacheKey) - : schema; + if (schema) { + return queryHasExtensions + ? this._extendSchema(schema, schemaPath, schemaCacheKey) + : schema; + } } // Read from disk @@ -757,15 +695,17 @@ export class GraphQLCache implements GraphQLCacheInterface { _getDefaultEndpoint( projectConfig: GraphQLProjectConfig, - ): ?{ endpointName: string, endpoint: GraphQLEndpoint } { + ): { endpointName: string; endpoint: GraphQLEndpoint } | null { // Jumping through hoops to get the default endpoint by name (needed for cache key) const endpointsExtension = projectConfig.endpointsExtension; if (!endpointsExtension) { return null; } - + // not public but needed + // @ts-ignore const defaultRawEndpoint = endpointsExtension.getRawEndpoint(); const rawEndpointsMap = endpointsExtension.getRawEndpointsMap(); + const endpointName = Object.keys(rawEndpointsMap).find( name => rawEndpointsMap[name] === defaultRawEndpoint, ); @@ -787,12 +727,12 @@ export class GraphQLCache implements GraphQLCacheInterface { readAllGraphQLFiles = async ( list: Array, ): Promise<{ - objectTypeDefinitions: Map, - fragmentDefinitions: Map, - graphQLFileMap: Map, + objectTypeDefinitions: Map; + fragmentDefinitions: Map; + graphQLFileMap: Map; }> => { const queue = list.slice(); // copy - const responses = []; + const responses: GraphQLFileInfo[] = []; while (queue.length) { const chunk = queue.splice(0, MAX_READS); const promises = chunk.map(fileInfo => @@ -810,13 +750,15 @@ export class GraphQLCache implements GraphQLCacheInterface { queue.push(fileInfo); } }) - .then(response => - responses.push({ - ...response, - mtime: fileInfo.mtime, - size: fileInfo.size, - }), - ), + .then((response: GraphQLFileInfo | void) => { + if (response) { + responses.push({ + ...response, + mtime: fileInfo.mtime, + size: fileInfo.size, + }); + } + }), ); await Promise.all(promises); // eslint-disable-line no-await-in-loop } @@ -831,9 +773,9 @@ export class GraphQLCache implements GraphQLCacheInterface { processGraphQLFiles = ( responses: Array, ): { - objectTypeDefinitions: Map, - fragmentDefinitions: Map, - graphQLFileMap: Map, + objectTypeDefinitions: Map; + fragmentDefinitions: Map; + graphQLFileMap: Map; } => { const objectTypeDefinitions = new Map(); const fragmentDefinitions = new Map(); @@ -877,21 +819,18 @@ export class GraphQLCache implements GraphQLCacheInterface { }); }); - return { objectTypeDefinitions, fragmentDefinitions, graphQLFileMap }; + return { + objectTypeDefinitions, + fragmentDefinitions, + graphQLFileMap, + }; }; /** * Returns a Promise to read a GraphQL file and return a GraphQL metadata * including a parsed AST. */ - promiseToReadGraphQLFile = ( - filePath: Uri, - ): Promise<{ - filePath: Uri, - content: string, - asts: Array, - queries: Array, - }> => { + promiseToReadGraphQLFile = (filePath: Uri): Promise => { return new Promise((resolve, reject) => fs.readFile(filePath, 'utf8', (error, content) => { if (error) { @@ -899,14 +838,21 @@ export class GraphQLCache implements GraphQLCacheInterface { return; } - const asts = []; - let queries = []; + const asts: DocumentNode[] = []; + let queries: CachedContent[] = []; if (content.trim().length !== 0) { try { queries = getQueryAndRange(content, filePath); if (queries.length === 0) { // still resolve with an empty ast - resolve({ filePath, content, asts: [], queries: [] }); + resolve({ + filePath, + content, + asts: [], + queries: [], + mtime: 0, + size: 0, + }); return; } @@ -918,14 +864,29 @@ export class GraphQLCache implements GraphQLCacheInterface { }), ), ); + resolve({ + filePath, + content, + asts, + queries, + mtime: 0, + size: 0, + }); } catch (_) { // If query has syntax errors, go ahead and still resolve // the filePath and the content, but leave ast empty. - resolve({ filePath, content, asts: [], queries: [] }); + resolve({ + filePath, + content, + asts: [], + queries: [], + mtime: 0, + size: 0, + }); return; } } - resolve({ filePath, content, asts, queries }); + resolve({ filePath, content, asts, queries, mtime: 0, size: 0 }); }), ); }; diff --git a/packages/graphql-language-service-server/src/GraphQLWatchman.js b/packages/graphql-language-service-server/src/GraphQLWatchman.js deleted file mode 100644 index 7542a734eba..00000000000 --- a/packages/graphql-language-service-server/src/GraphQLWatchman.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Copyright (c) 2019 GraphQL Contributors - * All rights reserved. - * - * This source code is licensed under the license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type { Uri } from 'graphql-language-service-types'; - -import watchman from 'fb-watchman'; - -export type WatchmanCommandResponse = { - version: string, - relative_path: Uri, - watcher: string, - watch: Uri, -}; - -export class GraphQLWatchman { - _client: watchman.Client; - constructor() { - this._client = new watchman.Client(); - } - - checkVersion(): Promise { - return new Promise((resolve, reject) => { - this._client.capabilityCheck( - { - optional: [], - required: ['cmd-watch-project'], - }, - (error, response) => { - if (error) { - reject(error); - } else { - // From the Watchman docs, response is something like: - // {'version': '3.8.0', 'capabilities': {'relative_root': true}}. - resolve(); - } - }, - ); - this._client.on('error', reject); - }); - } - - async listFiles( - entryPath: Uri, - options?: { [name: string]: any } = {}, - ): Promise> { - const { watch, relative_path } = await this.watchProject(entryPath); - const result = await this.runCommand('query', watch, { - expression: [ - 'allof', - ['type', 'f'], - ['anyof', ['match', '*.graphql'], ['match', '*.js']], - ['not', ['dirname', 'generated/relay']], - ['not', ['match', '**/__flow__/**', 'wholename']], - ['not', ['match', '**/__generated__/**', 'wholename']], - ['not', ['match', '**/__github__/**', 'wholename']], - ['not', ['match', '**/__mocks__/**', 'wholename']], - ['not', ['match', '**/node_modules/**', 'wholename']], - ['not', ['match', '**/__flowtests__/**', 'wholename']], - ['exists'], - ], - // Providing `path` will let watchman use path generator, and will perform - // a tree walk with respect to the relative_root and path provided. - // Path generator will do less work unless the root path of the repository - // is passed in as an entry path. - fields: ['name', 'size', 'mtime'], - relative_root: relative_path, - ...options, - }); - return result.files; - } - - runCommand(...args: Array): Promise { - return new Promise((resolve, reject) => - this._client.command(args, (error, response) => { - if (error) { - reject(error); - } - resolve(response); - }), - ).catch(error => { - throw new Error(error); - }); - } - - async watchProject(directoryPath: Uri): Promise { - try { - const response = await this.runCommand('watch-project', directoryPath); - return response; - } catch (error) { - throw new Error(error); - } - } - - async subscribe( - entryPath: Uri, - callback: (result: Object) => void, - ): Promise { - const { watch, relative_path } = await this.watchProject(entryPath); - - await this.runCommand('subscribe', watch, relative_path || watch, { - expression: ['allof', ['match', '*.graphql']], - fields: ['name', 'exists', 'size', 'mtime'], - relative_root: relative_path, - }); - - this._client.on('subscription', result => { - if (result.subscription !== relative_path) { - return; - } - callback(result); - }); - } - - dispose() { - this._client.end(); - this._client = null; - } -} diff --git a/packages/graphql-language-service-server/src/Logger.js b/packages/graphql-language-service-server/src/Logger.ts similarity index 88% rename from packages/graphql-language-service-server/src/Logger.js rename to packages/graphql-language-service-server/src/Logger.ts index d819e5db8cd..51ef7e78064 100644 --- a/packages/graphql-language-service-server/src/Logger.js +++ b/packages/graphql-language-service-server/src/Logger.ts @@ -5,16 +5,15 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type { Logger as VSCodeLogger } from 'vscode-jsonrpc'; +import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import fs from 'fs'; import os from 'os'; import { join } from 'path'; -const SEVERITY = { +const SEVERITY: { [key: string]: string } = { ERROR: 'ERROR', WARNING: 'WARNING', INFO: 'INFO', @@ -23,9 +22,9 @@ const SEVERITY = { export class Logger implements VSCodeLogger { _logFilePath: string; - _stream: ?fs.WriteStream; + _stream: fs.WriteStream | null; - constructor(): void { + constructor() { const dir = join(os.tmpdir(), 'graphql-language-service-logs'); try { if (!fs.existsSync(dir)) { @@ -69,7 +68,7 @@ export class Logger implements VSCodeLogger { const logMessage = `${timestamp} [${severity}] (pid: ${pid}) graphql-language-service-usage-logs: ${message}\n\n`; // write to the file in tmpdir - fs.appendFile(this._logFilePath, logMessage, error => {}); + fs.appendFile(this._logFilePath, logMessage, _error => {}); } } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.js b/packages/graphql-language-service-server/src/MessageProcessor.ts similarity index 79% rename from packages/graphql-language-service-server/src/MessageProcessor.js rename to packages/graphql-language-service-server/src/MessageProcessor.ts index 05c5146444e..c049b2c63e5 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.js +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -5,65 +5,58 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type { +import { CachedContent, - Diagnostic, - DidChangeWatchedFilesParams, GraphQLCache, - Range as RangeType, Uri, } from 'graphql-language-service-types'; + import { FileChangeTypeKind } from 'graphql-language-service-types'; import { extname, dirname } from 'path'; import { readFileSync } from 'fs'; import { URL } from 'url'; -import { - findGraphQLConfigFile, - getGraphQLConfig, - GraphQLProjectConfig, - GraphQLConfig, -} from 'graphql-config'; +import { findGraphQLConfigFile } from 'graphql-config'; import { GraphQLLanguageService } from 'graphql-language-service-interface'; -import { Position, Range } from 'graphql-language-service-utils'; -import { - CancellationToken, - NotificationMessage, - ServerCapabilities, -} from 'vscode-jsonrpc'; + +import { Range, Position } from 'graphql-language-service-utils'; + +import { CompletionParams, FileEvent } from 'vscode-languageserver-protocol'; + import { + Diagnostic, CompletionItem, - CompletionRequest, CompletionList, - DefinitionRequest, + CancellationToken, Hover, - HoverRequest, - InitializeRequest, InitializeResult, Location, PublishDiagnosticsParams, + DidChangeTextDocumentParams, + DidCloseTextDocumentParams, + DidChangeWatchedFilesParams, + InitializeParams, + Range as RangeType, + VersionedTextDocumentIdentifier, + DidSaveTextDocumentParams, + TextDocumentPositionParams, } from 'vscode-languageserver'; import { getGraphQLCache } from './GraphQLCache'; import { findGraphQLTags } from './findGraphQLTags'; import { Logger } from './Logger'; -import { GraphQLWatchman } from './GraphQLWatchman'; - -// Map { uri => { query, range } } type CachedDocumentType = { - version: number, - contents: Array, + version: number; + contents: CachedContent[]; }; export class MessageProcessor { - _graphQLCache: GraphQLCache; - _languageService: GraphQLLanguageService; + _graphQLCache!: GraphQLCache; + _languageService!: GraphQLLanguageService; _textDocumentCache: Map; - _watchmanClient: ?GraphQLWatchman; _isInitialized: boolean; @@ -71,25 +64,24 @@ export class MessageProcessor { _logger: Logger; - constructor(logger: Logger, watchmanClient: GraphQLWatchman): void { + constructor(logger: Logger) { this._textDocumentCache = new Map(); this._isInitialized = false; this._willShutdown = false; this._logger = logger; - this._watchmanClient = watchmanClient; } async handleInitializeRequest( - params: InitializeRequest.type, - token: CancellationToken, + params: InitializeParams, + _token?: CancellationToken, configDir?: string, - ): Promise { + ): Promise { if (!params) { throw new Error('`params` argument is required to initialize.'); } - const serverCapabilities: ServerCapabilities = { + const serverCapabilities: InitializeResult = { capabilities: { completionProvider: { resolveProvider: true }, definitionProvider: true, @@ -99,7 +91,9 @@ export class MessageProcessor { }; const rootPath = dirname( - findGraphQLConfigFile(configDir ? configDir.trim() : params.rootPath), + findGraphQLConfigFile( + configDir ? configDir.trim() : (params.rootPath as string), + ), ); if (!rootPath) { throw new Error( @@ -108,10 +102,6 @@ export class MessageProcessor { } this._graphQLCache = await getGraphQLCache(rootPath); - const config = getGraphQLConfig(rootPath); - if (this._watchmanClient) { - this._subcribeWatchman(config, this._watchmanClient); - } this._languageService = new GraphQLLanguageService(this._graphQLCache); if (!serverCapabilities) { @@ -130,57 +120,9 @@ export class MessageProcessor { return serverCapabilities; } - // Use watchman to subscribe to project file changes only if watchman is - // installed. Otherwise, rely on LSP watched files did change events. - async _subcribeWatchman( - config: GraphQLConfig, - watchmanClient: GraphQLWatchman, - ) { - if (!watchmanClient) { - return; - } - try { - // If watchman isn't installed, `GraphQLWatchman.checkVersion` will throw - await watchmanClient.checkVersion(); - - // Otherwise, subcribe watchman according to project config(s). - const projectMap = config.getProjects(); - let projectConfigs: GraphQLProjectConfig[] = projectMap - ? Object.values(projectMap) - : []; - - // There can either be a single config or one or more project - // configs, but not both. - if (projectConfigs.length === 0) { - projectConfigs = [config.getProjectConfig()]; - } - - // For each project config, subscribe to the file changes and update the - // cache accordingly. - projectConfigs.forEach((projectConfig: GraphQLProjectConfig) => { - watchmanClient.subscribe( - projectConfig.configDir, - this._graphQLCache.handleWatchmanSubscribeEvent( - config.configDir, - projectConfig, - ), - ); - }); - } catch (err) { - // If checkVersion raises {type: "ENOENT"}, watchman is not available. - // But it's okay to proceed. We'll use LSP watched file change notifications - // instead. If any other kind of error occurs, rethrow it up the call stack. - if (err.code === 'ENOENT') { - this._watchmanClient = undefined; - } else { - throw err; - } - } - } - async handleDidOpenOrSaveNotification( - params: NotificationMessage, - ): Promise { + params: DidSaveTextDocumentParams, + ): Promise { if (!this._isInitialized) { return null; } @@ -189,12 +131,12 @@ export class MessageProcessor { throw new Error('`textDocument` argument is required.'); } - const textDocument = params.textDocument; - const { text, uri } = textDocument; + const { text, textDocument } = params; + const { uri } = textDocument; - const diagnostics = []; + const diagnostics: Diagnostic[] = []; - let contents = []; + let contents: CachedContent[] = []; // Create/modify the cached entry if text is provided. // Otherwise, try searching the cache to perform diagnostics. @@ -238,8 +180,8 @@ export class MessageProcessor { } async handleDidChangeNotification( - params: NotificationMessage, - ): Promise { + params: DidChangeTextDocumentParams, + ): Promise { if (!this._isInitialized) { return null; } @@ -264,7 +206,7 @@ export class MessageProcessor { // As `contentChanges` is an array and we just want the // latest update to the text, grab the last entry from the array. - const uri = textDocument.uri || params.uri; + const uri = textDocument.uri; // If it's a .js file, try parsing the contents to see if GraphQL queries // exist. If not found, delete from the cache. @@ -282,7 +224,7 @@ export class MessageProcessor { this._updateObjectTypeDefinition(uri, contents); // Send the diagnostics onChange as well - const diagnostics = []; + const diagnostics: Diagnostic[] = []; await Promise.all( contents.map(async ({ query, range }) => { const results = await this._languageService.getDiagnostics(query, uri); @@ -306,7 +248,7 @@ export class MessageProcessor { return { uri, diagnostics }; } - handleDidCloseNotification(params: NotificationMessage): void { + handleDidCloseNotification(params: DidCloseTextDocumentParams): void { if (!this._isInitialized) { return; } @@ -344,9 +286,7 @@ export class MessageProcessor { process.exit(this._willShutdown ? 0 : 1); } - validateDocumentAndPosition( - params: CompletionRequest.type | HoverRequest.type, - ): void { + validateDocumentAndPosition(params: CompletionParams): void { if ( !params || !params.textDocument || @@ -360,11 +300,9 @@ export class MessageProcessor { } async handleCompletionRequest( - params: CompletionRequest.type, - token: CancellationToken, + params: CompletionParams, ): Promise> { if (!this._isInitialized) { - // $FlowFixMe return []; } @@ -392,7 +330,6 @@ export class MessageProcessor { // If there is no GraphQL query in this file, return an empty result. if (!found) { - // $FlowFixMe return []; } @@ -421,12 +358,9 @@ export class MessageProcessor { return { items: result, isIncomplete: false }; } - async handleHoverRequest( - params: HoverRequest.type, - token: CancellationToken, - ): Promise { + async handleHoverRequest(params: TextDocumentPositionParams): Promise { if (!this._isInitialized) { - return []; + return { contents: [] }; } this.validateDocumentAndPosition(params); @@ -448,7 +382,7 @@ export class MessageProcessor { // If there is no GraphQL query in this file, return an empty result. if (!found) { - return ''; + return { contents: [] }; } const { query, range } = found; @@ -469,13 +403,13 @@ export class MessageProcessor { async handleWatchedFilesChangedNotification( params: DidChangeWatchedFilesParams, - ): Promise { - if (!this._isInitialized || this._watchmanClient) { + ): Promise { + if (!this._isInitialized) { return null; } return Promise.all( - params.changes.map(async change => { + params.changes.map(async (change: FileEvent) => { if ( change.type === FileChangeTypeKind.Created || change.type === FileChangeTypeKind.Changed @@ -526,14 +460,16 @@ export class MessageProcessor { change.uri, false, ); + return { uri: change.uri, diagnostics: [] }; } + return { uri: change.uri, diagnostics: [] }; }), ); } async handleDefinitionRequest( - params: DefinitionRequest.type, - token: CancellationToken, + params: TextDocumentPositionParams, + _token?: CancellationToken, ): Promise> { if (!this._isInitialized) { return []; @@ -573,7 +509,7 @@ export class MessageProcessor { ); const formatted = result ? result.definitions.map(res => { - const defRange = res.range; + const defRange = res.range as Range; return { // TODO: fix this hack! // URI is being misused all over this library - there's a link that @@ -612,7 +548,7 @@ export class MessageProcessor { async _updateFragmentDefinition( uri: Uri, - contents: Array, + contents: CachedContent[], ): Promise { const rootDir = this._graphQLCache.getGraphQLConfig().configDir; @@ -625,7 +561,7 @@ export class MessageProcessor { async _updateObjectTypeDefinition( uri: Uri, - contents: Array, + contents: CachedContent[], ): Promise { const rootDir = this._graphQLCache.getGraphQLConfig().configDir; @@ -636,7 +572,7 @@ export class MessageProcessor { ); } - _getCachedDocument(uri: string): ?CachedDocumentType { + _getCachedDocument(uri: string): CachedDocumentType | null { if (this._textDocumentCache.has(uri)) { const cachedDocument = this._textDocumentCache.get(uri); if (cachedDocument) { @@ -648,13 +584,17 @@ export class MessageProcessor { } _invalidateCache( - textDocument: Object, + textDocument: VersionedTextDocumentIdentifier, uri: Uri, - contents: Array, + contents: CachedContent[], ): void { if (this._textDocumentCache.has(uri)) { const cachedDocument = this._textDocumentCache.get(uri); - if (cachedDocument && cachedDocument.version < textDocument.version) { + if ( + cachedDocument && + textDocument.version && + cachedDocument.version < textDocument.version + ) { // Current server capabilities specify the full sync of the contents. // Therefore always overwrite the entire content. this._textDocumentCache.set(uri, { @@ -662,7 +602,7 @@ export class MessageProcessor { contents, }); } - } else { + } else if (textDocument.version) { this._textDocumentCache.set(uri, { version: textDocument.version, contents, @@ -678,10 +618,7 @@ export class MessageProcessor { // Check the uri to determine the file type (JavaScript/GraphQL). // If .js file, either return the parsed query/range or null if GraphQL queries // are not found. -export function getQueryAndRange( - text: string, - uri: string, -): Array { +export function getQueryAndRange(text: string, uri: string): CachedContent[] { // Check if the text content includes a GraphQLV query. // If the text doesn't include GraphQL queries, do not proceed. if (extname(uri) === '.js') { @@ -709,15 +646,16 @@ export function getQueryAndRange( } function processDiagnosticsMessage( - results: Array, + results: Diagnostic[], query: string, - range: ?RangeType, -): Array { + range: RangeType | null, +): Diagnostic[] { 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 => + // @ts-ignore diagnostic.range.end.lessThanOrEqualTo(lastCharacterPosition), ); diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.js b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts similarity index 75% rename from packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.js rename to packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts index 3023bebce19..6d00892401e 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.js +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts @@ -5,7 +5,6 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ import { GraphQLSchema } from 'graphql/type'; @@ -16,18 +15,18 @@ import fetchMock from 'fetch-mock'; import { GraphQLCache } from '../GraphQLCache'; import { getQueryAndRange } from '../MessageProcessor'; -function wihtoutASTNode(definition: object) { +function wihtoutASTNode(definition: any) { const result = { ...definition }; delete result.astNode; return result; } describe('GraphQLCache', () => { - let cache; - let graphQLRC; + const configDir = __dirname; + let graphQLRC = getGraphQLConfig(configDir); + let cache = new GraphQLCache(configDir, graphQLRC); beforeEach(async () => { - const configDir = __dirname; graphQLRC = getGraphQLConfig(configDir); cache = new GraphQLCache(configDir, graphQLRC); }); @@ -78,7 +77,9 @@ describe('GraphQLCache', () => { }); it('extend the schema with appropriate custom directive', async () => { - const schema = await cache.getSchema('testWithCustomDirectives'); + const schema = (await cache.getSchema( + 'testWithCustomDirectives', + )) as GraphQLSchema; expect( wihtoutASTNode(schema.getDirective('customDirective')), ).toMatchObject( @@ -94,7 +95,7 @@ describe('GraphQLCache', () => { }); it('extend the schema with appropriate custom directive 2', async () => { - const schema = await cache.getSchema('testWithSchema'); + const schema = (await cache.getSchema('testWithSchema')) as GraphQLSchema; expect( wihtoutASTNode(schema.getDirective('customDirective')), ).toMatchObject( @@ -110,74 +111,6 @@ describe('GraphQLCache', () => { }); }); - describe('handleWatchmanSubscribeEvent', () => { - it('handles invalidating the schema cache', async () => { - const projectConfig = graphQLRC.getProjectConfig('testWithSchema'); - await cache.getSchema('testWithSchema'); - expect(cache._schemaMap.size).toEqual(1); - const handler = cache.handleWatchmanSubscribeEvent( - __dirname, - projectConfig, - ); - const testResult = { - root: __dirname, - subscription: '', - files: [ - { - name: '__schema__/StarWarsSchema.graphql', - exists: true, - size: 5, - is_fresh_instance: true, - mtime: Date.now(), - }, - ], - }; - handler(testResult); - expect(cache._schemaMap.size).toEqual(0); - }); - - it('handles invalidating the endpoint cache', async () => { - const projectConfig = graphQLRC.getProjectConfig( - 'testWithEndpointAndSchema', - ); - const introspectionResult = await graphQLRC - .getProjectConfig('testWithSchema') - .resolveIntrospection(); - - fetchMock.mock({ - matcher: '*', - response: { - headers: { - 'Content-Type': 'application/json', - }, - body: introspectionResult, - }, - }); - - await cache.getSchema('testWithEndpointAndSchema'); - expect(cache._schemaMap.size).toEqual(1); - const handler = cache.handleWatchmanSubscribeEvent( - __dirname, - projectConfig, - ); - const testResult = { - root: __dirname, - subscription: '', - files: [ - { - name: '__schema__/StarWarsSchema.graphql', - exists: true, - size: 5, - is_fresh_instance: true, - mtime: Date.now(), - }, - ], - }; - handler(testResult); - expect(cache._schemaMap.size).toEqual(0); - }); - }); - describe('getFragmentDependencies', () => { const duckContent = `fragment Duck on Duck { cuack diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.js b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts similarity index 76% rename from packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.js rename to packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index 663e9b3ad89..54348c92a30 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.js +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -5,23 +5,23 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ import { Position, Range } from 'graphql-language-service-utils'; import { MessageProcessor, getQueryAndRange } from '../MessageProcessor'; -jest.mock('../GraphQLWatchman'); -import { GraphQLWatchman } from '../GraphQLWatchman'; -import { GraphQLConfig } from 'graphql-config'; +jest.mock('../Logger'); + +import { DefinitionQueryResult } from 'graphql-language-service-types'; + +import { Logger } from '../Logger'; describe('MessageProcessor', () => { - const mockWatchmanClient = new GraphQLWatchman(); - const messageProcessor = new MessageProcessor(undefined, mockWatchmanClient); + const logger = new Logger(); + const messageProcessor = new MessageProcessor(logger); const queryDir = `${__dirname}/__queries__`; - const schemaPath = `${__dirname}/__schema__/StarWarsSchema.graphql`; const textDocumentTestString = ` { hero(episode: NEWHOPE){ @@ -29,16 +29,9 @@ describe('MessageProcessor', () => { } `; - const initialDocument = { - textDocument: { - text: textDocumentTestString, - uri: `${queryDir}/test.graphql`, - version: 0, - }, - }; - beforeEach(() => { messageProcessor._graphQLCache = { + // @ts-ignore getGraphQLConfig() { return { configDir: __dirname, @@ -47,22 +40,31 @@ describe('MessageProcessor', () => { }, }; }, + // @ts-ignore updateFragmentDefinition() {}, + // @ts-ignore updateObjectTypeDefinition() {}, + // @ts-ignore handleWatchmanSubscribeEvent() {}, }; messageProcessor._languageService = { + // @ts-ignore getAutocompleteSuggestions: (query, position, uri) => { return [{ label: `${query} at ${uri}` }]; }, + // @ts-ignore getDiagnostics: (query, uri) => { return []; }, - getDefinition: (query, position, uri) => { + getDefinition: async ( + _query, + position, + uri, + ): Promise => { return { + queryRange: [new Range(position, position)], definitions: [ { - uri, position, path: uri, }, @@ -71,13 +73,26 @@ describe('MessageProcessor', () => { }, }; }); + + const initialDocument = { + textDocument: { + text: textDocumentTestString, + uri: `${queryDir}/test.graphql`, + version: 0, + }, + }; + messageProcessor._isInitialized = true; - messageProcessor._logger = { log() {} }; it('initializes properly and opens a file', async () => { - const { capabilities } = await messageProcessor.handleInitializeRequest({ - rootPath: __dirname, - }); + const { capabilities } = await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootUri: __dirname, + }, + null, + __dirname, + ); expect(capabilities.definitionProvider).toEqual(true); expect(capabilities.completionProvider.resolveProvider).toEqual(true); expect(capabilities.textDocumentSync).toEqual(1); @@ -87,6 +102,7 @@ describe('MessageProcessor', () => { const uri = `${queryDir}/test2.graphql`; const query = 'test'; messageProcessor._textDocumentCache.set(uri, { + version: 0, contents: [ { query, @@ -99,7 +115,6 @@ describe('MessageProcessor', () => { position: new Position(0, 0), textDocument: { uri }, }; - const result = await messageProcessor.handleCompletionRequest(test); expect(result).toEqual({ items: [{ label: `${query} at ${uri}` }], @@ -110,6 +125,7 @@ describe('MessageProcessor', () => { it('properly changes the file cache with the didChange handler', async () => { const uri = `file://${queryDir}/test.graphql`; messageProcessor._textDocumentCache.set(uri, { + version: 1, contents: [ { query: '', @@ -127,6 +143,7 @@ describe('MessageProcessor', () => { const result = await messageProcessor.handleDidChangeNotification({ textDocument: { + // @ts-ignore text: textDocumentTestString, uri, version: 1, @@ -150,10 +167,10 @@ describe('MessageProcessor', () => { return messageProcessor .handleCompletionRequest(params) .then(result => expect(result).toEqual(null)) - .catch(error => {}); + .catch(() => {}); }); - // Doesn't work with mock watchman client + // modified to work with jest.mock() of WatchmanClient it('runs definition requests', async () => { const validQuery = ` { @@ -167,9 +184,18 @@ describe('MessageProcessor', () => { textDocument: { text: validQuery, uri: `${queryDir}/test3.graphql`, - version: 0, + version: 1, }, }; + messageProcessor._getCachedDocument = (_uri: string) => ({ + version: 1, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(20, 4)), + }, + ], + }); await messageProcessor.handleDidOpenOrSaveNotification(newDocument); @@ -182,44 +208,6 @@ describe('MessageProcessor', () => { await expect(result[0].uri).toEqual(`file://${queryDir}/test3.graphql`); }); - it('loads configs without projects when watchman is present', async () => { - const config = new GraphQLConfig( - { - schemaPath, - includes: `${queryDir}/*.graphql`, - }, - 'not/a/real/config', - ); - - await messageProcessor._subcribeWatchman(config, mockWatchmanClient); - await expect(mockWatchmanClient.subscribe).toBeCalledTimes(1); - await expect(mockWatchmanClient.subscribe).toBeCalledWith( - 'not/a/real', - undefined, - ); - }); - - it('loads configs with projects when watchman is present', async () => { - const config = new GraphQLConfig( - { - projects: { - foo: { - schemaPath, - includes: `${queryDir}/*.graphql`, - }, - }, - }, - 'not/a/real/config', - ); - - await messageProcessor._subcribeWatchman(config, mockWatchmanClient); - await expect(mockWatchmanClient.subscribe).toBeCalledTimes(1); - await expect(mockWatchmanClient.subscribe).toBeCalledWith( - 'not/a/real', - undefined, - ); - }); - it('getQueryAndRange finds queries in tagged templates', async () => { const text = ` // @flow diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.js b/packages/graphql-language-service-server/src/findGraphQLTags.js deleted file mode 100644 index 9dcc1cab4b5..00000000000 --- a/packages/graphql-language-service-server/src/findGraphQLTags.js +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Copyright (c) 2019 GraphQL Contributors - * All rights reserved. - * - * This source code is licensed under the license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import { Position, Range } from 'graphql-language-service-utils'; - -import { parse } from '@babel/parser'; - -// Attempt to be as inclusive as possible of source text. -const PARSER_OPTIONS = { - allowImportExportEverywhere: true, - allowReturnOutsideFunction: true, - allowSuperOutsideMethod: true, - sourceType: 'module', - plugins: [ - 'flow', - 'jsx', - 'doExpressions', - 'objectRestSpread', - ['decorators', { decoratorsBeforeExport: false }], - 'classProperties', - 'classPrivateProperties', - 'classPrivateMethods', - 'exportDefaultFrom', - 'exportNamespaceFrom', - 'asyncGenerators', - 'functionBind', - 'functionSent', - 'dynamicImport', - 'numericSeparator', - 'optionalChaining', - 'importMeta', - 'bigInt', - 'optionalCatchBinding', - 'throwExpressions', - ['pipelineOperator', { proposal: 'minimal' }], - 'nullishCoalescingOperator', - ], - strictMode: false, -}; - -export function findGraphQLTags( - text: string, -): Array<{ tag: string, template: string, range: Range }> { - const result = []; - const ast = parse(text, PARSER_OPTIONS); - - const visitors = { - CallExpression: node => { - const callee = node.callee; - if ( - !( - (callee.type === 'Identifier' && - CREATE_CONTAINER_FUNCTIONS[callee.name]) || - (callee.kind === 'MemberExpression' && - callee.object.type === 'Identifier' && - callee.object.value === 'Relay' && - callee.property.type === 'Identifier' && - CREATE_CONTAINER_FUNCTIONS[callee.property.name]) - ) - ) { - traverse(node, visitors); - return; - } - const fragments = node.arguments[1]; - if (fragments.type === 'ObjectExpression') { - fragments.properties.forEach(property => { - const tagName = getGraphQLTagName(property.value.tag); - const template = getGraphQLText(property.value.quasi); - if (tagName) { - const loc = property.value.loc; - const range = new Range( - new Position(loc.start.line - 1, loc.start.column), - new Position(loc.end.line - 1, loc.end.column), - ); - result.push({ - tag: tagName, - template, - range, - }); - } - }); - } else { - const tagName = getGraphQLTagName(fragments.tag); - const template = getGraphQLText(fragments.quasi); - if (tagName) { - const loc = fragments.loc; - const range = new Range( - new Position(loc.start.line - 1, loc.start.column), - new Position(loc.end.line - 1, loc.end.column), - ); - result.push({ - tag: tagName, - template, - range, - }); - } - } - - // Visit remaining arguments - for (let ii = 2; ii < node.arguments.length; ii++) { - visit(node.arguments[ii], visitors); - } - }, - TaggedTemplateExpression: node => { - const tagName = getGraphQLTagName(node.tag); - if (tagName) { - const loc = node.quasi.quasis[0].loc; - const range = new Range( - new Position(loc.start.line - 1, loc.start.column), - new Position(loc.end.line - 1, loc.end.column), - ); - result.push({ - tag: tagName, - template: node.quasi.quasis[0].value.raw, - range, - }); - } - }, - }; - visit(ast, visitors); - return result; -} - -const CREATE_CONTAINER_FUNCTIONS = { - createFragmentContainer: true, - createPaginationContainer: true, - createRefetchContainer: true, -}; - -const IDENTIFIERS = { graphql: true, gql: true }; - -const IGNORED_KEYS = { - comments: true, - end: true, - leadingComments: true, - loc: true, - name: true, - start: true, - trailingComments: true, - type: true, -}; - -function getGraphQLTagName(tag) { - if (tag.type === 'Identifier' && IDENTIFIERS.hasOwnProperty(tag.name)) { - return tag.name; - } else if ( - tag.type === 'MemberExpression' && - tag.object.type === 'Identifier' && - tag.object.name === 'graphql' && - tag.property.type === 'Identifier' && - tag.property.name === 'experimental' - ) { - return 'graphql.experimental'; - } - return null; -} - -function getGraphQLText(quasi) { - const quasis = quasi.quasis; - return quasis[0].value.raw; -} - -function visit(node, visitors) { - const fn = visitors[node.type]; - if (fn != null) { - fn(node); - return; - } - traverse(node, visitors); -} - -function traverse(node, visitors) { - for (const key in node) { - if (IGNORED_KEYS[key]) { - continue; - } - const prop = node[key]; - if (prop && typeof prop === 'object' && typeof prop.type === 'string') { - visit(prop, visitors); - } else if (Array.isArray(prop)) { - prop.forEach(item => { - if (item && typeof item === 'object' && typeof item.type === 'string') { - visit(item, visitors); - } - }); - } - } -} diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.ts b/packages/graphql-language-service-server/src/findGraphQLTags.ts new file mode 100644 index 00000000000..2155374c31f --- /dev/null +++ b/packages/graphql-language-service-server/src/findGraphQLTags.ts @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2019 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { + Expression, + TaggedTemplateExpression, + ObjectExpression, + TemplateLiteral, +} from '@babel/types'; + +import { Position, Range } from 'graphql-language-service-utils'; + +import { parse, ParserOptions } from '@babel/parser'; + +// Attempt to be as inclusive as possible of source text. +const PARSER_OPTIONS: ParserOptions = { + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, + sourceType: 'module', + plugins: [ + 'flow', + 'jsx', + 'doExpressions', + 'objectRestSpread', + ['decorators', { decoratorsBeforeExport: false }], + 'classProperties', + 'classPrivateProperties', + 'classPrivateMethods', + 'exportDefaultFrom', + 'exportNamespaceFrom', + 'asyncGenerators', + 'functionBind', + 'functionSent', + 'dynamicImport', + 'numericSeparator', + 'optionalChaining', + 'importMeta', + 'bigInt', + 'optionalCatchBinding', + 'throwExpressions', + ['pipelineOperator', { proposal: 'minimal' }], + 'nullishCoalescingOperator', + ], + strictMode: false, +}; + +const CREATE_CONTAINER_FUNCTIONS: { [key: string]: boolean } = { + createFragmentContainer: true, + createPaginationContainer: true, + createRefetchContainer: true, +}; + +type TagResult = { tag: string; template: string; range: Range }; + +interface TagVisitiors { + [type: string]: (node: any) => void; +} + +export function findGraphQLTags(text: string): TagResult[] { + const result: TagResult[] = []; + const ast = parse(text, PARSER_OPTIONS); + + const visitors = { + CallExpression: (node: Expression) => { + if ('callee' in node) { + const callee = node.callee; + if ( + !( + (callee.type === 'Identifier' && + CREATE_CONTAINER_FUNCTIONS[callee.name]) || + (callee.kind === 'MemberExpression' && + callee.object.type === 'Identifier' && + callee.object.value === 'Relay' && + callee.property.type === 'Identifier' && + CREATE_CONTAINER_FUNCTIONS[callee.property.name]) + ) + ) { + traverse(node, visitors); + return; + } + + if ('arguments' in node) { + const fragments = node.arguments[1]; + if (fragments.type === 'ObjectExpression') { + fragments.properties.forEach( + (property: ObjectExpression['properties'][0]) => { + if ( + 'value' in property && + 'loc' in property.value && + 'tag' in property.value + ) { + const tagName = getGraphQLTagName(property.value.tag); + const template = getGraphQLText(property.value.quasi); + if (tagName && property.value.loc) { + const loc = property.value.loc; + const range = new Range( + new Position(loc.start.line - 1, loc.start.column), + new Position(loc.end.line - 1, loc.end.column), + ); + result.push({ + tag: tagName, + template, + range, + }); + } + } + }, + ); + } else if ('tag' in fragments) { + const tagName = getGraphQLTagName(fragments.tag); + const template = getGraphQLText(fragments.quasi); + if (tagName && fragments.loc) { + const loc = fragments.loc; + const range = new Range( + new Position(loc.start.line - 1, loc.start.column), + new Position(loc.end.line - 1, loc.end.column), + ); + result.push({ + tag: tagName, + template, + range, + }); + } + } + // Visit remaining arguments + for (let ii = 2; ii < node.arguments.length; ii++) { + visit(node.arguments[ii], visitors); + } + } + } + }, + TaggedTemplateExpression: (node: TaggedTemplateExpression) => { + const tagName = getGraphQLTagName(node.tag); + if (tagName) { + const loc = node.quasi.quasis[0].loc; + if (loc) { + const range = new Range( + new Position(loc.start.line - 1, loc.start.column), + new Position(loc.end.line - 1, loc.end.column), + ); + result.push({ + tag: tagName, + template: node.quasi.quasis[0].value.raw, + range, + }); + } + } + }, + }; + visit(ast, visitors); + return result; +} + +const IDENTIFIERS = { graphql: true, gql: true }; + +const IGNORED_KEYS: { [key: string]: boolean } = { + comments: true, + end: true, + leadingComments: true, + loc: true, + name: true, + start: true, + trailingComments: true, + type: true, +}; + +function getGraphQLTagName(tag: Expression): string | null { + if (tag.type === 'Identifier' && IDENTIFIERS.hasOwnProperty(tag.name)) { + return tag.name; + } else if ( + tag.type === 'MemberExpression' && + tag.object.type === 'Identifier' && + tag.object.name === 'graphql' && + tag.property.type === 'Identifier' && + tag.property.name === 'experimental' + ) { + return 'graphql.experimental'; + } + return null; +} + +function getGraphQLText(quasi: TemplateLiteral) { + const quasis = quasi.quasis; + return quasis[0].value.raw; +} + +function visit(node: { [key: string]: any }, visitors: TagVisitiors) { + const fn = visitors[node.type]; + if (fn && fn != null) { + fn(node); + return; + } + traverse(node, visitors); +} + +function traverse(node: { [key: string]: any }, visitors: TagVisitiors) { + for (const key in node) { + if (IGNORED_KEYS[key]) { + continue; + } + const prop = node[key]; + if (prop && typeof prop === 'object' && typeof prop.type === 'string') { + visit(prop, visitors); + } else if (Array.isArray(prop)) { + prop.forEach(item => { + if (item && typeof item === 'object' && typeof item.type === 'string') { + visit(item, visitors); + } + }); + } + } +} diff --git a/packages/graphql-language-service-server/src/index.js b/packages/graphql-language-service-server/src/index.ts similarity index 88% rename from packages/graphql-language-service-server/src/index.js rename to packages/graphql-language-service-server/src/index.ts index 8bb04e5b942..46374189d0f 100644 --- a/packages/graphql-language-service-server/src/index.js +++ b/packages/graphql-language-service-server/src/index.ts @@ -5,13 +5,10 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ export { GraphQLCache, getGraphQLCache } from './GraphQLCache'; -export { GraphQLWatchman } from './GraphQLWatchman'; - export { MessageProcessor } from './MessageProcessor'; export { default as startServer } from './startServer'; diff --git a/packages/graphql-language-service-server/src/startServer.js b/packages/graphql-language-service-server/src/startServer.ts similarity index 89% rename from packages/graphql-language-service-server/src/startServer.js rename to packages/graphql-language-service-server/src/startServer.ts index 65ec592bdcf..a6b8f4a1046 100644 --- a/packages/graphql-language-service-server/src/startServer.js +++ b/packages/graphql-language-service-server/src/startServer.ts @@ -5,12 +5,9 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ import net from 'net'; - -import { GraphQLWatchman } from './GraphQLWatchman'; import { MessageProcessor } from './MessageProcessor'; import { @@ -43,11 +40,20 @@ import { import { Logger } from './Logger'; type Options = { - port?: number, - method?: string, - configDir?: string, + // port for the LSP server to run on + port?: number; + // socket, streams, or node (ipc). if socket, port is required + method?: 'socket' | 'stream' | 'node'; + // the directory where graphql-config is found + configDir?: string; }; +/** + * startServer - initialize LSP server with options + * + * @param options {Options} server initialization methods + * @returns {Promise} + */ export default (async function startServer(options: Options): Promise { const logger = new Logger(); @@ -64,7 +70,6 @@ export default (async function startServer(options: Options): Promise { '--port is required to establish socket connection.', ); process.exit(1); - return; } const port = options.port; @@ -78,7 +83,7 @@ export default (async function startServer(options: Options): Promise { process.exit(0); }); const connection = createMessageConnection(reader, writer, logger); - addHandlers(connection, options.configDir, logger); + addHandlers(connection, logger, options.configDir); connection.listen(); }) .listen(port); @@ -94,17 +99,17 @@ export default (async function startServer(options: Options): Promise { break; } const connection = createMessageConnection(reader, writer, logger); - addHandlers(connection, options.configDir, logger); + addHandlers(connection, logger, options.configDir); connection.listen(); } }); function addHandlers( connection: MessageConnection, - configDir?: string, logger: Logger, + configDir?: string, ): void { - const messageProcessor = new MessageProcessor(logger, new GraphQLWatchman()); + const messageProcessor = new MessageProcessor(logger); connection.onNotification( DidOpenTextDocumentNotification.type, async params => { diff --git a/packages/graphql-language-service-server/src/stringToHash.js b/packages/graphql-language-service-server/src/stringToHash.ts similarity index 88% rename from packages/graphql-language-service-server/src/stringToHash.js rename to packages/graphql-language-service-server/src/stringToHash.ts index e8b5a7bd9b4..cbf49ad1070 100644 --- a/packages/graphql-language-service-server/src/stringToHash.js +++ b/packages/graphql-language-service-server/src/stringToHash.ts @@ -5,11 +5,10 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ /* eslint-disable no-bitwise */ -export default function(str: string): number { +export default function stringToHash(str: string): number { let hash = 0; if (str.length === 0) { return hash; diff --git a/packages/graphql-language-service-server/tsconfig.esm.json b/packages/graphql-language-service-server/tsconfig.esm.json new file mode 100644 index 00000000000..26049771033 --- /dev/null +++ b/packages/graphql-language-service-server/tsconfig.esm.json @@ -0,0 +1,19 @@ +{ + "extends": "../../resources/tsconfig.base.esm.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./esm" + }, + "references": [ + { + "path": "../graphql-language-service-types" + }, + { + "path": "../graphql-language-service-interface" + }, + { + "path": "../graphql-language-service-utils" + } + ], + "exclude": ["**/__tests__/**", "**/*.spec.*"] +} diff --git a/packages/graphql-language-service-server/tsconfig.json b/packages/graphql-language-service-server/tsconfig.json new file mode 100644 index 00000000000..43b788d8c47 --- /dev/null +++ b/packages/graphql-language-service-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../resources/tsconfig.base.cjs.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "references": [ + { + "path": "../graphql-language-service-types" + }, + { + "path": "../graphql-language-service-interface" + }, + { + "path": "../graphql-language-service-utils" + } + ], + "exclude": ["**/__tests__/**", "**/*.spec.*"] +} diff --git a/packages/graphql-language-service-types/src/index.ts b/packages/graphql-language-service-types/src/index.ts index a10d8ccdefa..9435fb3d488 100644 --- a/packages/graphql-language-service-types/src/index.ts +++ b/packages/graphql-language-service-types/src/index.ts @@ -6,7 +6,11 @@ * LICENSE file in the root directory of this source tree. * */ - +import { + Diagnostic as DiagnosticType, + Position as PositionType, + CompletionItem as CompletionItemType, +} from 'vscode-languageserver-protocol'; import { GraphQLSchema, KindEnum } from 'graphql'; import { ASTNode, @@ -46,7 +50,7 @@ export interface CharacterStream { pattern: TokenPattern, consume?: boolean | null | undefined, caseFold?: boolean | null | undefined, - ) => Array | boolean; + ) => string[] | boolean; backUp: (num: number) => void; column: () => number; indentation: () => number; @@ -68,8 +72,8 @@ export type GraphQLProjectConfiguration = { // For multiple applications with overlapping files, // these configuration options may be helpful - includes?: Array; - excludes?: Array; + includes?: string[]; + excludes?: string[]; // If you'd like to specify any other configurations, // we provide a reserved namespace for it @@ -85,13 +89,13 @@ export interface GraphQLCache { getObjectTypeDependencies: ( query: string, - fragmentDefinitions: Map | null | undefined, - ) => Promise>; + fragmentDefinitions: Map, + ) => Promise; getObjectTypeDependenciesForAST: ( parsedQuery: ASTNode, fragmentDefinitions: Map, - ) => Promise>; + ) => Promise; getObjectTypeDefinitions: ( graphQLConfig: GraphQLProjectConfig, @@ -100,24 +104,24 @@ export interface GraphQLCache { updateObjectTypeDefinition: ( rootDir: Uri, filePath: Uri, - contents: Array, - ) => Promise; + contents: CachedContent[], + ) => Promise; updateObjectTypeDefinitionCache: ( rootDir: Uri, filePath: Uri, exists: boolean, - ) => Promise; + ) => Promise; getFragmentDependencies: ( query: string, fragmentDefinitions: Map | null | undefined, - ) => Promise>; + ) => Promise; getFragmentDependenciesForAST: ( parsedQuery: ASTNode, fragmentDefinitions: Map, - ) => Promise>; + ) => Promise; getFragmentDefinitions: ( graphQLConfig: GraphQLProjectConfig, @@ -126,32 +130,27 @@ export interface GraphQLCache { updateFragmentDefinition: ( rootDir: Uri, filePath: Uri, - contents: Array, - ) => Promise; + contents: CachedContent[], + ) => Promise; updateFragmentDefinitionCache: ( rootDir: Uri, filePath: Uri, exists: boolean, - ) => Promise; + ) => Promise; getSchema: ( - appName: string | null | undefined, - queryHasExtensions?: boolean | null | undefined, - ) => Promise; - - handleWatchmanSubscribeEvent: ( - rootDir: string, - projectConfig: GraphQLProjectConfig, - ) => (result: Object) => undefined; + appName?: string, + queryHasExtensions?: boolean, + ) => Promise; } // online-parser related -export interface Position { +export type Position = PositionType & { line: number; character: number; - lessThanOrEqualTo: (position: Position) => boolean; -} + lessThanOrEqualTo?: (position: Position) => boolean; +}; export interface Range { start: Position; @@ -161,7 +160,7 @@ export interface Range { export type CachedContent = { query: string; - range: Range | null | undefined; + range: Range | null; }; export type RuleOrString = Rule | string; @@ -210,7 +209,7 @@ export type RuleKind = export type State = { level: number; - levels?: Array; + levels?: number[]; prevState: State | null | undefined; rule: ParseRule | null | undefined; kind: RuleKind | null | undefined; @@ -234,7 +233,8 @@ export type GraphQLFileMetadata = { export type GraphQLFileInfo = { filePath: Uri; content: string; - asts: Array; + asts: DocumentNode[]; + queries: CachedContent[]; size: number; mtime: number; }; @@ -255,7 +255,7 @@ export type AllTypeInfo = { fieldDef: GraphQLField | null | undefined; enumValue: GraphQLEnumValue | null | undefined; argDef: GraphQLArgument | null | undefined; - argDefs: Array | null | undefined; + argDefs: GraphQLArgument[] | null | undefined; objectFieldDefs: GraphQLInputFieldMap | null | undefined; }; @@ -281,23 +281,11 @@ export type CustomValidationRule = ( context: ValidationContext, ) => Record; -export type Diagnostic = { - range: Range; - severity?: number; - code?: number | string; - source?: string; - message: string; -}; +export type Diagnostic = DiagnosticType; -export type CompletionItem = { - label: string; - kind?: number; - detail?: string; - sortText?: string; - documentation?: string | null | undefined; - // GraphQL Deprecation information - isDeprecated?: boolean | null | undefined; - deprecationReason?: string | null | undefined; +export type CompletionItem = CompletionItemType & { + isDeprecated?: boolean; + deprecationReason?: string; }; // Below are basically a copy-paste from Nuclide rpc types for definitions. @@ -314,8 +302,8 @@ export type Definition = { }; export type DefinitionQueryResult = { - queryRange: Array; - definitions: Array; + queryRange: Range[]; + definitions: Definition[]; }; // Outline view @@ -334,7 +322,7 @@ export type TextToken = { value: string | undefined; }; -export type TokenizedText = Array; +export type TokenizedText = TextToken[]; export type OutlineTree = { // Must be one or the other. If both are present, tokenizedText is preferred. plainText?: string; @@ -343,17 +331,13 @@ export type OutlineTree = { startPosition: Position; endPosition?: Position; - children: Array; + children: OutlineTree[]; }; export type Outline = { - outlineTrees: Array; + outlineTrees: OutlineTree[]; }; -export interface DidChangeWatchedFilesParams { - changes: FileEvent[]; -} - export interface FileEvent { uri: string; type: FileChangeType; diff --git a/packages/graphql-language-service-types/tsconfig.json b/packages/graphql-language-service-types/tsconfig.json index 8366ff17b05..5009847bfae 100644 --- a/packages/graphql-language-service-types/tsconfig.json +++ b/packages/graphql-language-service-types/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../resources/tsconfig.base.cjs.json", "compilerOptions": { + "composite": true, "rootDir": "./src", "outDir": "./dist" }, diff --git a/packages/graphql-language-service-utils/src/file.ts b/packages/graphql-language-service-utils/src/file.ts index a26e7cbb8ff..b0fa937ac4f 100644 --- a/packages/graphql-language-service-utils/src/file.ts +++ b/packages/graphql-language-service-utils/src/file.ts @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2019 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + export function getFileExtension(filePath: string): string | null { const pathParts = /^.+\.([^.]+)$/.exec(filePath); // if there's a file extension diff --git a/packages/graphql-language-service-utils/tsconfig.esm.json b/packages/graphql-language-service-utils/tsconfig.esm.json index 2b0cb75205e..911c3eaeab6 100644 --- a/packages/graphql-language-service-utils/tsconfig.esm.json +++ b/packages/graphql-language-service-utils/tsconfig.esm.json @@ -2,7 +2,8 @@ "extends": "../../resources/tsconfig.base.esm.json", "compilerOptions": { "rootDir": "./src", - "outDir": "./esm" + "outDir": "./esm", + "composite": true }, "references": [ { diff --git a/packages/graphql-language-service-utils/tsconfig.json b/packages/graphql-language-service-utils/tsconfig.json index 5471e4971b9..b4eaa168349 100644 --- a/packages/graphql-language-service-utils/tsconfig.json +++ b/packages/graphql-language-service-utils/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../resources/tsconfig.base.cjs.json", "compilerOptions": { "rootDir": "./src", - "outDir": "./dist" + "outDir": "./dist", + "composite": true }, "references": [ { diff --git a/packages/graphql-language-service/README.md b/packages/graphql-language-service/README.md index b81fe5a864b..0c4beb26003 100644 --- a/packages/graphql-language-service/README.md +++ b/packages/graphql-language-service/README.md @@ -5,13 +5,13 @@ ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/codemirror-graphql) [![License](https://img.shields.io/npm/l/graphql-language-service.svg?style=flat-square)](LICENSE) -_This is currently in technical preview. We welcome your feedback and suggestions._ +_We welcome your feedback and suggestions._ GraphQL Language Service provides an interface for building GraphQL language services for IDEs. Partial support for [Microsoft's Language Server Protocol](https://github.com/Microsoft/language-server-protocol) is in place, with more to come in the future. -Currently supported features include: +Supported features include: - Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**) - Autocomplete suggestions (**spec-compliant**) @@ -22,7 +22,9 @@ Currently supported features include: ### Dependencies -GraphQL Language Service depends on [Watchman](https://facebook.github.io/watchman/) running on your machine. Follow [this installation guide](https://facebook.github.io/watchman/docs/install.html) to install Watchman. +An LSP compatible client with it's own file watcher, that sends watch notifications to the server. + +**DROPPED**: GraphQL Language Service no longer depends on [Watchman](https://facebook.github.io/watchman/) ### Installation @@ -40,6 +42,11 @@ The library includes a node executable file which you can find in `./node_module Check out [graphql-config](https://github.com/graphcool/graphql-config) +The graphql features we support are: + +- `customDirectives` - `['@myExampleDirective']` +- `customValidationRules` - returns rules array with parameter `ValidationContext` from `graphql/validation`; + ### Using the command-line interface The node executable contains several commands: `server` and a command-line language service methods (`lint`, `autocomplete`, `outline`). diff --git a/packages/graphql-language-service/package.json b/packages/graphql-language-service/package.json index fe4779147b1..f40493f6022 100644 --- a/packages/graphql-language-service/package.json +++ b/packages/graphql-language-service/package.json @@ -26,7 +26,7 @@ ], "scripts": { "test": "node ../../resources/runTests.js", - "build": "yarn run build-js && yarn build-esm && yarn run build-flow", + "build": "tsc && yarn run build-flow", "build-js": "node ../../resources/buildJs.js", "build-esm": "ESM=true babel src --root-mode upward --ignore __tests__ --out-dir esm", "build-flow": "node ../../resources/buildFlow.js" diff --git a/packages/graphql-language-service/src/__tests__/index-test.js b/packages/graphql-language-service/src/__tests__/index-test.ts similarity index 100% rename from packages/graphql-language-service/src/__tests__/index-test.js rename to packages/graphql-language-service/src/__tests__/index-test.ts diff --git a/packages/graphql-language-service/src/cli.js b/packages/graphql-language-service/src/cli.ts similarity index 90% rename from packages/graphql-language-service/src/cli.js rename to packages/graphql-language-service/src/cli.ts index 6c87c8bc75f..aaa705c54bd 100644 --- a/packages/graphql-language-service/src/cli.js +++ b/packages/graphql-language-service/src/cli.ts @@ -5,13 +5,12 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ import yargs from 'yargs'; import client from './client'; + import { Logger, startServer } from 'graphql-language-service-server'; -import watchman from 'fb-watchman'; const { argv } = yargs .usage( @@ -95,6 +94,10 @@ const { argv } = yargs const command = argv._.pop(); +if (!command) { + throw Error('no command supplied'); +} + switch (command) { case 'server': process.on('uncaughtException', error => { @@ -103,17 +106,8 @@ switch (command) { ); process.exit(0); }); - const watchmanClient = new watchman.Client(); - watchmanClient.capabilityCheck({}, (error, res) => { - if (error) { - process.stderr.write( - `Cannot find installed watchman service with an error: ${error}`, - ); - process.exit(0); - } - }); - const options = {}; + const options: { [key: string]: any } = {}; if (argv && argv.port) { options.port = argv.port; } @@ -130,9 +124,10 @@ switch (command) { logger.error(error); } break; - default: - client(command, argv); + default: { + client(command, argv as { [key: string]: string }); break; + } } // Exit the process when stream closes from remote end. diff --git a/packages/graphql-language-service/src/client.js b/packages/graphql-language-service/src/client.ts similarity index 81% rename from packages/graphql-language-service/src/client.js rename to packages/graphql-language-service/src/client.ts index 08c619d312c..8081e809488 100644 --- a/packages/graphql-language-service/src/client.js +++ b/packages/graphql-language-service/src/client.ts @@ -5,10 +5,9 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ -import type { GraphQLSchema } from 'graphql'; +import { GraphQLSchema } from 'graphql'; import invariant from 'assert'; import fs from 'fs'; @@ -20,6 +19,7 @@ import { } from 'graphql-language-service-interface'; import { Position } from 'graphql-language-service-utils'; import path from 'path'; +import { CompletionItem, Diagnostic } from 'graphql-language-service-types/src'; const GRAPHQL_SUCCESS_CODE = 0; const GRAPHQL_FAILURE_CODE = 1; @@ -37,7 +37,10 @@ type EXIT_CODE = 0 | 1; * Query validation is only performed if a schema path is supplied. */ -export default function main(command: string, argv: Object): void { +export default function main( + command: string, + argv: { [key: string]: string }, +): void { const filePath = argv.file && argv.file.trim(); invariant( argv.text || argv.file, @@ -51,8 +54,9 @@ export default function main(command: string, argv: Object): void { switch (command) { case 'autocomplete': const lines = text.split('\n'); - const row = argv.row || lines.length - 1; - const column = argv.column || lines[lines.length - 1].length; + const row = parseInt(argv.row, 10) || lines.length - 1; + const column = + parseInt(argv.column, 10) || lines[lines.length - 1].length; const point = new Position(row, column); exitCode = _getAutocompleteSuggestions(text, point, schemaPath); break; @@ -69,6 +73,10 @@ export default function main(command: string, argv: Object): void { process.exit(exitCode); } +interface AutocompleteResultsMap { + [i: number]: CompletionItem; +} + function _getAutocompleteSuggestions( queryText: string, point: Position, @@ -84,10 +92,13 @@ function _getAutocompleteSuggestions( const resultArray = schema ? getAutocompleteSuggestions(schema, queryText, point) : []; - const resultObject = resultArray.reduce((prev, cur, index) => { - prev[index] = cur; - return prev; - }, {}); + const resultObject: AutocompleteResultsMap = resultArray.reduce( + (prev: AutocompleteResultsMap, cur, index) => { + prev[index] = cur; + return prev; + }, + {}, + ); process.stdout.write(JSON.stringify(resultObject, null, 2)); return GRAPHQL_SUCCESS_CODE; } catch (error) { @@ -96,8 +107,12 @@ function _getAutocompleteSuggestions( } } +interface DiagnosticResultsMap { + [i: number]: Diagnostic; +} + function _getDiagnostics( - filePath: string, + _filePath: string, queryText: string, schemaPath?: string, ): EXIT_CODE { @@ -106,10 +121,13 @@ function _getDiagnostics( // whether the query text is syntactically valid. const schema = schemaPath ? generateSchema(schemaPath) : null; const resultArray = getDiagnostics(queryText, schema); - const resultObject = resultArray.reduce((prev, cur, index) => { - prev[index] = cur; - return prev; - }, {}); + const resultObject: DiagnosticResultsMap = resultArray.reduce( + (prev: DiagnosticResultsMap, cur, index) => { + prev[index] = cur; + return prev; + }, + {}, + ); process.stdout.write(JSON.stringify(resultObject, null, 2)); return GRAPHQL_SUCCESS_CODE; } catch (error) { diff --git a/packages/graphql-language-service/tsconfig.esm.json b/packages/graphql-language-service/tsconfig.esm.json new file mode 100644 index 00000000000..6391c2ad1f1 --- /dev/null +++ b/packages/graphql-language-service/tsconfig.esm.json @@ -0,0 +1,23 @@ +{ + "extends": "../../resources/tsconfig.base.esm.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./esm" + }, + "references": [ + { + "path": "../graphql-language-service-server" + }, + { + "path": "../graphql-language-service-interface" + }, + { + "path": "../graphql-language-service-utils" + }, + { + "path": "../graphql-language-service-types" + } + ], + "exclude": ["**/__tests__/**", "**/*.spec.*"] +} diff --git a/packages/graphql-language-service/tsconfig.json b/packages/graphql-language-service/tsconfig.json new file mode 100644 index 00000000000..1e6b9056393 --- /dev/null +++ b/packages/graphql-language-service/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../resources/tsconfig.base.cjs.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "references": [ + { + "path": "../graphql-language-service-server" + }, + { + "path": "../graphql-language-service-interface" + }, + { + "path": "../graphql-language-service-utils" + }, + { + "path": "../graphql-language-service-types" + } + ], + "exclude": ["**/__tests__/**", "**/*.spec.*"] +} diff --git a/resources/tsconfig.build.cjs.json b/resources/tsconfig.build.cjs.json index a9a768e736c..85e7cb3c4eb 100644 --- a/resources/tsconfig.build.cjs.json +++ b/resources/tsconfig.build.cjs.json @@ -19,6 +19,12 @@ }, { "path": "../packages/graphql-language-service-interface" + }, + { + "path": "../packages/graphql-language-service-server" + }, + { + "path": "../packages/graphql-language-service" } ] } diff --git a/resources/tsconfig.build.esm.json b/resources/tsconfig.build.esm.json index dc34a687e95..50d4a1cc213 100644 --- a/resources/tsconfig.build.esm.json +++ b/resources/tsconfig.build.esm.json @@ -19,6 +19,12 @@ }, { "path": "../packages/graphql-language-service-interface/tsconfig.esm.json" + }, + { + "path": "../packages/graphql-language-service-server/tsconfig.esm.json" + }, + { + "path": "../packages/graphql-language-service/tsconfig.esm.json" } ] } diff --git a/resources/util.js b/resources/util.js index ff165875a9a..77cdc5eedb4 100644 --- a/resources/util.js +++ b/resources/util.js @@ -5,7 +5,6 @@ * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * - * @flow */ 'use strict'; diff --git a/yarn.lock b/yarn.lock index 5745e747320..d7621b8c070 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3049,6 +3049,11 @@ resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/fetch-mock@^7.3.2": + version "7.3.2" + resolved "https://registry.npmjs.org/@types/fetch-mock/-/fetch-mock-7.3.2.tgz#58805ba36a9357be92cc8c008dbfda937e9f7d8f" + integrity sha512-NCEfv49jmDsBAixjMjEHKVgmVQlJ+uK56FOc+2roYPExnXCZDpi6mJOHQ3v23BiO84hBDStND9R2itJr7PNoow== + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"