Skip to content

Commit

Permalink
feat: typescript, tsx, jsx support for LSP server using babel (#1427)
Browse files Browse the repository at this point in the history
* introduce typescript, tsx, jsx support to LSP server
* expose document parser to server init config
* remove use of constants, improve error handling, null extensions
* pass parser, fileExtensions, update lsp server docs
  • Loading branch information
acao committed Mar 20, 2020
1 parent 218a4d0 commit ee06123
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 181 deletions.
97 changes: 74 additions & 23 deletions packages/graphql-language-service-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Supported features include:
- Autocomplete suggestions (**spec-compliant**)
- Hyperlink to fragment definitions and named types (type, input, enum) definitions (**spec-compliant**)
- Outline view support for queries
- Support for `gql` `graphql` and other template tags inside javascript, typescript, jsx and tsx files, and an interface to allow custom parsing of all files.

## Installation and Usage

Expand All @@ -41,11 +42,81 @@ The library includes a node executable file which you can find in `./node_module

Check out [graphql-config](https://graphql-config.com/docs/introduction)

#### `.graphqlrc` or `.graphqlrc.yml/yaml`

```yaml
schema: 'packages/api/src/schema.graphql'
documents: 'packages/app/src/components/**/*.graphql'
extensions:
customExtension:
foo: true
```

#### `.graphqlrc` or `.graphqlrc.json`

```json
{ "schema": "https://localhost:8000" }
```

#### `graphql.config.js` or `.graphqlrc.js`

```js
module.exports = { schema: 'https://localhost:8000' };
```

#### custom `loadConfig`

use graphql config [`loadConfig`](https://graphql-config.com/docs/load-config) for further customization:

```ts
import { loadConfig } from 'graphql-config'; // 3.0.0 or later!

await startServer({
method: 'node',
config: loadConfig({
// myPlatform.config.js works now!
configName: 'myPlatform',
// or instead of configName, an exact path (relative from rootDir or absolute)
filePath: 'exact/path/to/config.js (also supports yml, json)'
// rootDir to look for config file(s), or for relative resolution for exact `filePath`. default process.cwd()
rootDir: '',
})
});
```

The graphql features we support are:

- `customDirectives` - `['@myExampleDirective']`
- `customValidationRules` - returns rules array with parameter `ValidationContext` from `graphql/validation`;

### Usage

Initialize the GraphQL Language Server with the `startServer` function:

```ts
import { startServer } from 'graphql-language-service-server';

await startServer({
method: 'node',
});
```

If you are developing a service or extension, this is the LSP language server you want to run.

When developing vscode extensions, just the above is enough to get started for your extension's `ServerOptions.run.module`, for example.

`startServer` function takes the following parameters:

| Parameter | Required | Description |
| -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------- |
| port | `true` when method is `socket`, `false` otherwise | port for the LSP server to run on |
| method | `false` | `socket`, `streams`, or `node` (ipc) |
| config | `false` | custom `graphql-config` instance from `loadConfig` (see example above) |
| configDir | `false` | the directory where graphql-config is found |
| extensions | `false` | array of functions to transform the graphql-config and add extensions dynamically |
| parser | `false` | Customize _all_ file parsing by overriding the default `parseDocument` function |
| fileExtensions | `false`. defaults to `['.js', '.ts', '.tsx, '.jsx']` | Customize file extensions used by the default LSP parser |

## Architectural Overview

GraphQL Language Service currently communicates via Stream transport with the IDE server. GraphQL server will receive/send RPC messages to perform language service features, while caching the necessary GraphQL artifacts such as fragment definitions, GraphQL schemas etc. More about the server interface and RPC message format below.
Expand Down Expand Up @@ -80,27 +151,7 @@ For each transport, there is a slight difference in JSON message format, especia
| ---------------: | ---------------------------- | ------------------------------------------- |
| Diagnostics | `getDiagnostics` | `textDocument/publishDiagnostics` |
| Autocompletion | `getAutocompleteSuggestions` | `textDocument/completion` |
| Outline | `getOutline` | Not supported yet |
| Go-to definition | `getDefinition` | Not supported yet |
| Outline | `getOutline` | `textDocument/outline` |
| Document Symbols | `getDocumentSymbols` | `textDocument/symbols` |
| Go-to definition | `getDefinition` | `textDocument/definition` |
| File Events | Not supported yet | `didOpen/didClose/didSave/didChange` events |

#### startServer

The GraphQL Language Server can be started with the following function:

```ts
import { startServer } from 'graphql-language-service-server';

await startServer({
method: 'node',
});
```

`startServer` function takes the following parameters:

| Parameter | Required | Description |
| ---------- | ------------------------------------------------- | --------------------------------------------------------------------------------- |
| port | `true` when method is `socket`, `false` otherwise | port for the LSP server to run on |
| method | `false` | socket, streams, or node (ipc) |
| configDir | `false` | the directory where graphql-config is found |
| extensions | `false` | array of functions to transform the graphql-config and add extensions dynamically |
8 changes: 4 additions & 4 deletions packages/graphql-language-service-server/src/GraphQLCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
GraphQLConfig,
GraphQLProjectConfig,
} from 'graphql-config';
import { getQueryAndRange } from './MessageProcessor';
import { parseDocument } from './parseDocument';
import stringToHash from './stringToHash';
import glob from 'glob';

Expand Down Expand Up @@ -59,7 +59,7 @@ export async function getGraphQLCache(
): Promise<GraphQLCacheInterface> {
let graphQLConfig = config || (await loadConfig({ rootDir: configDir }));
if (extensions && extensions.length > 0) {
for (const extension of extensions) {
for await (const extension of extensions) {
graphQLConfig = await extension(graphQLConfig);
}
}
Expand Down Expand Up @@ -633,7 +633,7 @@ export class GraphQLCache implements GraphQLCacheInterface {
schema = await projectConfig.getSchema();
}

const customDirectives = projectConfig.extensions.customDirectives;
const customDirectives = projectConfig?.extensions?.customDirectives;
if (customDirectives && schema) {
const directivesSDL = customDirectives.join('\n\n');
schema = extendSchema(
Expand Down Expand Up @@ -796,7 +796,7 @@ export class GraphQLCache implements GraphQLCacheInterface {
let queries: CachedContent[] = [];
if (content.trim().length !== 0) {
try {
queries = getQueryAndRange(content, filePath);
queries = parseDocument(content, filePath);
if (queries.length === 0) {
// still resolve with an empty ast
resolve({
Expand Down
5 changes: 3 additions & 2 deletions packages/graphql-language-service-server/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ 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 => {});
process.stderr.write(logMessage, err => {
console.error(err);
// const processSt = (severity === SEVERITY.ERROR) ? process.stderr : process.stdout
process.stderr.write(logMessage, _err => {
// console.error(err);
});
}
}
Expand Down
69 changes: 17 additions & 52 deletions packages/graphql-language-service-server/src/MessageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*
*/

import { extname } from 'path';
import { readFileSync } from 'fs';
import { URL } from 'url';

Expand Down Expand Up @@ -52,45 +51,42 @@ import {
} from 'vscode-languageserver';

import { getGraphQLCache } from './GraphQLCache';
import { findGraphQLTags } from './findGraphQLTags';
import { parseDocument } from './parseDocument';

import { Logger } from './Logger';

type CachedDocumentType = {
version: number;
contents: CachedContent[];
};

// const KIND_TO_SYMBOL_KIND = {
// Field: SymbolKind.Field,
// OperationDefinition: SymbolKind.Class,
// FragmentDefinition: SymbolKind.Class,
// FragmentSpread: SymbolKind.Struct,
// };

export class MessageProcessor {
_graphQLCache!: GraphQLCache;
_graphQLConfig: GraphQLConfig | undefined;
_languageService!: GraphQLLanguageService;
_textDocumentCache: Map<string, CachedDocumentType>;

_isInitialized: boolean;

_willShutdown: boolean;

_logger: Logger;
_extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>;
_fileExtensions?: Array<string>;
_parser: typeof parseDocument;

constructor(
logger: Logger,
extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>,
config?: GraphQLConfig,
parser?: typeof parseDocument,
fileExtensions?: string[],
) {
this._textDocumentCache = new Map();
this._isInitialized = false;
this._willShutdown = false;
this._logger = logger;
this._extensions = extensions;
this._fileExtensions = fileExtensions;
this._graphQLConfig = config;
this._parser = parser || parseDocument;
}

async handleInitializeRequest(
Expand Down Expand Up @@ -164,10 +160,11 @@ export class MessageProcessor {
if ('text' in textDocument && textDocument.text) {
// textDocument/didSave does not pass in the text content.
// Only run the below function if text is passed in.
contents = getQueryAndRange(textDocument.text, uri);
contents = parseDocument(textDocument.text, uri, this._fileExtensions);

this._invalidateCache(textDocument, uri, contents);
} else {
const cachedDocument = this._getCachedDocument(uri);
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (cachedDocument) {
contents = cachedDocument.contents;
}
Expand Down Expand Up @@ -233,8 +230,11 @@ export class MessageProcessor {

// If it's a .js file, try parsing the contents to see if GraphQL queries
// exist. If not found, delete from the cache.
const contents = getQueryAndRange(contentChange.text, uri);

const contents = parseDocument(
contentChange.text,
uri,
this._fileExtensions,
);
// If it's a .graphql file, proceed normally and invalidate the cache.
this._invalidateCache(textDocument, uri, contents);

Expand Down Expand Up @@ -449,7 +449,7 @@ export class MessageProcessor {
) {
const uri = change.uri;
const text: string = readFileSync(new URL(uri).pathname).toString();
const contents = getQueryAndRange(text, uri);
const contents = parseDocument(text, uri, this._fileExtensions);

this._updateFragmentDefinition(uri, contents);
this._updateObjectTypeDefinition(uri, contents);
Expand Down Expand Up @@ -660,7 +660,6 @@ export class MessageProcessor {

return null;
}

_invalidateCache(
textDocument: VersionedTextDocumentIdentifier,
uri: Uri,
Expand Down Expand Up @@ -689,40 +688,6 @@ export class MessageProcessor {
}
}

/**
* Helper functions to perform requested services from client/server.
*/

// 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): 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') {
if (
text.indexOf('graphql`') === -1 &&
text.indexOf('graphql.experimental`') === -1 &&
text.indexOf('gql`') === -1
) {
return [];
}
const templates = findGraphQLTags(text);
return templates.map(({ template, range }) => ({ query: template, range }));
} else {
const query = text;
if (!query && query !== '') {
return [];
}
const lines = query.split('\n');
const range = new Range(
new Position(0, 0),
new Position(lines.length - 1, lines[lines.length - 1].length - 1),
);
return [{ query, range }];
}
}

function processDiagnosticsMessage(
results: Diagnostic[],
query: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
TypeDefinitionNode,
} from 'graphql';
import { GraphQLCache, getGraphQLCache } from '../GraphQLCache';
import { getQueryAndRange } from '../MessageProcessor';
import { parseDocument } from '../parseDocument';
import { FragmentInfo, ObjectTypeInfo } from 'graphql-language-service-types';

function wihtoutASTNode(definition: any) {
Expand Down Expand Up @@ -161,7 +161,7 @@ describe('GraphQLCache', () => {
' `,\n' +
' },\n' +
'});';
const contents = getQueryAndRange(text, 'test.js');
const contents = parseDocument(text, 'test.js');
const result = await cache.getFragmentDependenciesForAST(
parse(contents[0].query),
fragmentDefinitions,
Expand Down

0 comments on commit ee06123

Please sign in to comment.