Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphql-config): add graphql config extensions #1118

Merged
merged 11 commits into from
Mar 14, 2020
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Node.JS CI
on: [push]
on: [push, pull_request]
jobs:
test:
name: lint & test
Expand Down
99 changes: 99 additions & 0 deletions packages/graphql-language-service-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,102 @@
[![License](https://img.shields.io/npm/l/graphql-language-service-server.svg?style=flat-square)](LICENSE)

Server process backing the [GraphQL Language Service](https://github.com/graphql/graphiql/tree/master/packages/graphql-language-service).

GraphQL Language Service Server 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.

Supported features include:

- Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**)
- Autocomplete suggestions (**spec-compliant**)
- Hyperlink to fragment definitions and named types (type, input, enum) definitions (**spec-compliant**)
- Outline view support for queries

## Installation and Usage

### Dependencies

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

```
git clone git@github.com:graphql/graphql-language-servic-server.git
cd {path/to/your/repo}
npm install ../graphql-language-service-server
```

After pulling the latest changes from this repo, be sure to run `yarn run build` to transform the `src/` directory and generate the `dist/` directory.

The library includes a node executable file which you can find in `./node_modules/.bin/graphql.js` after installation.

### GraphQL configuration file (`.graphqlrc.yml`)

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

The graphql features we support are:

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

## 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.

The IDE server should launch a separate GraphQL server with its own child process for each `.graphqlrc.yml` file the IDE finds (using the nearest ancestor directory relative to the file currently being edited):

```
./application

./productA
.graphqlrc.yml
ProductAQuery.graphql
ProductASchema.graphql

./productB
.graphqlrc.yml
ProductBQuery.graphql
ProductBSchema.graphql
```

A separate GraphQL server should be instantiated for `ProductA` and `ProductB`, each with its own `.graphqlrc.yml` file, as illustrated in the directory structure above.

The IDE server should manage the lifecycle of the GraphQL server. Ideally, the IDE server should spawn a child process for each of the GraphQL Language Service processes necessary, and gracefully exit the processes as the IDE closes. In case of errors or a sudden halt the GraphQL Language Service will close as the stream from the IDE closes.

### Server Interface

GraphQL Language Server uses [JSON-RPC](http://www.jsonrpc.org/specification) to communicate with the IDE servers. Microsoft's language server currently supports two communication transports: Stream (stdio) and IPC. For IPC transport, the reference guide to be used for development is [the language server protocol](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md) documentation.

For each transport, there is a slight difference in JSON message format, especially in how the methods to be invoked are defined - below are the currently supported methods for each transport (will be updated as progress is made):

| | Stream | IPC |
| ---------------: | ---------------------------- | ------------------------------------------- |
| Diagnostics | `getDiagnostics` | `textDocument/publishDiagnostics` |
| Autocompletion | `getAutocompleteSuggestions` | `textDocument/completion` |
| Outline | `getOutline` | Not supported yet |
| Go-to definition | `getDefinition` | Not supported yet |
| 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: 7 additions & 1 deletion packages/graphql-language-service-server/src/GraphQLCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ const {

export async function getGraphQLCache(
configDir: Uri,
extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>,
): Promise<GraphQLCacheInterface> {
const graphQLConfig = await loadConfig({ rootDir: configDir });
let graphQLConfig = await loadConfig({ rootDir: configDir });
if (extensions && extensions.length > 0) {
for (const extension of extensions) {
graphQLConfig = await extension(graphQLConfig);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@divyenduz so this is intended to re-declare graphQLConfig on every invocation? I'm guessing the result of await extension(graphQLConfig) returns the previous config plus the extension?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as of now, the signature of an extension is a function that takes a GraphQLConfig as input and returns a GraphQL config as its output with the modification (extension) applied.

extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>
Which implies, extension: (config: GraphQLConfig) => GraphQLConfig

}
}
return new GraphQLCache(configDir, graphQLConfig);
}

Expand Down
10 changes: 8 additions & 2 deletions packages/graphql-language-service-server/src/MessageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Uri,
FileChangeTypeKind,
DefinitionQueryResult,
GraphQLConfig,
} from 'graphql-language-service-types';

import { GraphQLLanguageService } from 'graphql-language-service-interface';
Expand Down Expand Up @@ -76,12 +77,17 @@ export class MessageProcessor {
_willShutdown: boolean;

_logger: Logger;
_extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>;

constructor(logger: Logger) {
constructor(
logger: Logger,
extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>,
) {
this._textDocumentCache = new Map();
this._isInitialized = false;
this._willShutdown = false;
this._logger = logger;
this._extensions = extensions;
}

async handleInitializeRequest(
Expand Down Expand Up @@ -111,7 +117,7 @@ export class MessageProcessor {
);
}

this._graphQLCache = await getGraphQLCache(rootPath);
this._graphQLCache = await getGraphQLCache(rootPath, this._extensions);
this._languageService = new GraphQLLanguageService(this._graphQLCache);

if (!serverCapabilities) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
FragmentDefinitionNode,
TypeDefinitionNode,
} from 'graphql';
import { GraphQLCache } from '../GraphQLCache';
import { GraphQLCache, getGraphQLCache } from '../GraphQLCache';
import { getQueryAndRange } from '../MessageProcessor';
import { FragmentInfo, ObjectTypeInfo } from 'graphql-language-service-types';

Expand All @@ -43,6 +43,22 @@ describe('GraphQLCache', () => {
fetchMock.restore();
});

describe('getGraphQLCache', () => {
it('should apply extensions', async () => {
const extension = config => {
return {
...config,
extension: 'extension-used', // Just adding a key to the config to demo extension usage
};
};
const extensions = [extension];
const cacheWithExtensions = await getGraphQLCache(configDir, extensions);
const config = cacheWithExtensions.getGraphQLConfig();
expect('extension' in config).toBe(true);
expect((config as any).extension).toBe('extension-used');
});
});

describe('getSchema', () => {
it('generates the schema correctly for the test app config', async () => {
const schema = await cache.getSchema('testWithSchema');
Expand Down
16 changes: 13 additions & 3 deletions packages/graphql-language-service-server/src/startServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import * as net from 'net';
import { MessageProcessor } from './MessageProcessor';
import { GraphQLConfig } from 'graphql-config';

import {
createMessageConnection,
Expand Down Expand Up @@ -49,7 +50,10 @@ type Options = {
method?: 'socket' | 'stream' | 'node';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks incorrect. I think this should be 'socket' | 'stream' | 'node'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed via f6d8a56

// the directory where graphql-config is found
configDir?: string;
// array of functions to transform the graphql-config and add extensions dynamically
extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>;
};
('graphql-language-service-types');

/**
* startServer - initialize LSP server with options
Expand Down Expand Up @@ -86,7 +90,12 @@ export default async function startServer(options: Options): Promise<void> {
process.exit(0);
});
const connection = createMessageConnection(reader, writer, logger);
addHandlers(connection, logger, options.configDir);
addHandlers(
connection,
logger,
options.configDir,
options.extensions,
);
connection.listen();
})
.listen(port);
Expand All @@ -102,7 +111,7 @@ export default async function startServer(options: Options): Promise<void> {
break;
}
const connection = createMessageConnection(reader, writer, logger);
addHandlers(connection, logger, options.configDir);
addHandlers(connection, logger, options.configDir, options.extensions);
connection.listen();
}
}
Expand All @@ -111,8 +120,9 @@ function addHandlers(
connection: MessageConnection,
logger: Logger,
configDir?: string,
extensions?: Array<(config: GraphQLConfig) => GraphQLConfig>,
): void {
const messageProcessor = new MessageProcessor(logger);
const messageProcessor = new MessageProcessor(logger, extensions);
connection.onNotification(
DidOpenTextDocumentNotification.type,
async params => {
Expand Down
42 changes: 3 additions & 39 deletions packages/graphql-language-service/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# GraphQL Language Service
# graphql-language-service

> Note: This package will soon be renamed to graphql-language-service-cli

[![NPM](https://img.shields.io/npm/v/graphql-language-service.svg)](https://npmjs.com/graphql-language-service)
![npm downloads](https://img.shields.io/npm/dm/graphql-language-service?label=npm%20downloads)
Expand Down Expand Up @@ -91,41 +93,3 @@ Options:
At least one command is required.
Commands: "server, validate, autocomplete, outline"
```

## 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.

The IDE server should launch a separate GraphQL server with its own child process for each `.graphqlrc.yml` file the IDE finds (using the nearest ancestor directory relative to the file currently being edited):

```
./application

./productA
.graphqlrc.yml
ProductAQuery.graphql
ProductASchema.graphql

./productB
.graphqlrc.yml
ProductBQuery.graphql
ProductBSchema.graphql
```

A separate GraphQL server should be instantiated for `ProductA` and `ProductB`, each with its own `.graphqlrc.yml` file, as illustrated in the directory structure above.

The IDE server should manage the lifecycle of the GraphQL server. Ideally, the IDE server should spawn a child process for each of the GraphQL Language Service processes necessary, and gracefully exit the processes as the IDE closes. In case of errors or a sudden halt the GraphQL Language Service will close as the stream from the IDE closes.

### Server Interface

GraphQL Language Server uses [JSON-RPC](http://www.jsonrpc.org/specification) to communicate with the IDE servers. Microsoft's language server currently supports two communication transports: Stream (stdio) and IPC. For IPC transport, the reference guide to be used for development is [the language server protocol](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md) documentation.

For each transport, there is a slight difference in JSON message format, especially in how the methods to be invoked are defined - below are the currently supported methods for each transport (will be updated as progress is made):

| | Stream | IPC |
| ---------------: | ---------------------------- | ------------------------------------------- |
| Diagnostics | `getDiagnostics` | `textDocument/publishDiagnostics` |
| Autocompletion | `getAutocompleteSuggestions` | `textDocument/completion` |
| Outline | `getOutline` | Not supported yet |
| Go-to definition | `getDefinition` | Not supported yet |
| File Events | Not supported yet | `didOpen/didClose/didSave/didChange` events |
Loading