Skip to content

Commit

Permalink
feat: allow disabling validation (diagnostics)
Browse files Browse the repository at this point in the history
users receive duplicate validation messages when using our LSP alongside existing validation tools like `graphql-eslint`
  • Loading branch information
acao committed May 1, 2022
1 parent b7c29d4 commit d51fe75
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-teachers-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-language-service-server': patch
---

users receive duplicate validation messages when using our LSP alongside existing validation tools like graphql-eslint
86 changes: 73 additions & 13 deletions packages/graphql-language-service-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,40 +126,100 @@ await startServer({
});
```

<span id="custom-graphql-config" />

#### Custom `graphql-config` features

The graphql-config features we support are:

```js
module.exports = {
extensions: {
// add customDirectives *legacy*. you can now provide multiple schema pointers to config.schema/project.schema, including inline strings
// add customDirectives (legacy). you can now provide multiple schema pointers to config.schema/project.schema, including inline strings. same with scalars or any SDL type that you'd like to append to the schema
customDirectives: ['@myExampleDirective'],
// a function that returns rules array with parameter `ValidationContext` from `graphql/validation`
// a function that returns an array of validation rules, ala https://github.com/graphql/graphql-js/tree/main/src/validation/rules
// note that this file will be loaded by the vscode runtime, so the node version and other factors will come into play
customValidationRules: require('./config/customValidationRules'),
languageService: {
// should the language service read schema for lookups from a cached file based on graphql config output?
cacheSchemaFileForLookup: true,
// should the language service read schema for definition lookups from a cached file based on graphql config output?
// NOTE: this will disable all definition lookup for local SDL files
cacheSchemaFileForLookup: true,
// undefined by default which has the same effect as `true`, set to `false` if you are already using // `graphql-eslint` or some other tool for validating graphql in your IDE. Must be explicitly `false` to disable this feature, not just "falsy"
enableValidation: true,
},
},
};
```

we also load `require('dotenv').config()`, so you can use process.env variables from local `.env` files!
or for multi-project workspaces:

```ts
// graphql.config.ts
export default {
projects: {
myProject: {
schema: [
// internally in `graphql-config`, an attempt will be made to combine these schemas into one in-memory schema to use for validation, lookup, etc
'http://localhost:8080',
'./myproject/schema.graphql',
'./myproject/schema.ts',
'@customDirective(arg: String!)',
'scalar CustomScalar',
],
// project specific defaults
extensions: {
languageService: {
cacheSchemaFileForLookup: true,
enableValidation: false,
},
},
},
anotherProject: {
schema: {
'http://localhost:8081': {
customHeaders: { Authorization: 'Bearer example' },
},
},
},
},
// global defaults for all projects
extensions: {
languageService: {
cacheSchemaFileForLookup: false,
enableValidation: true,
},
},
};
```

You can specify any of these settings globally as above, or per project. Read the graphql-config docs to learn more about this!

For secrets (headers, urls, etc), you can import `dotenv()` and set a basepath as you wish in your `graphql-config` file to preload `process.env` variables.

### Troubleshooting notes

- you may need to manually restart the language server for some of these configurations to take effect
- graphql-config's multi-project support is not related to multi-root workspaces in vscode - in fact, each workspace can have multiple graphql config projects, which is what makes multi-root workspaces tricky to support. coming soon!

<span id="workspace-configuration" />

### Workspace Configuration

The LSP Server reads config by sending `workspace/configuration` method when it initializes.

| Parameter | Default | Description |
| ---------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graphql-config.load.baseDir` | workspace root or process.cwd() | the path where graphql config looks for config files |
| `graphql-config.load.filePath` | `null` | exact filepath of the config file. |
| `graphql-config.load.configName` | `graphql` | config name prefix instead of `graphql` |
| `graphql-config.load.legacy` | `true` | backwards compatibility with `graphql-config@2` |
| `graphql-config.dotEnvPath` | `null` | backwards compatibility with `graphql-config@2` |
| `vsode-graphql.cacheSchemaFileForLookup` | `false` | generate an SDL file based on your graphql-config schema configuration for schema definition lookup and other features. useful when your `schema` config are urls |
Note: We still do not support LSP multi-root workspaces but will tackle this very soon!

Many LSP clients beyond vscode offer ways to set these configurations, such as via `initializationOptions` in nvim.coc.
The options are mostly designed to configure graphql-config's load parameters, the only thing we can't configure with graphql config. The final option can be set in `graphql-config` as well

| Parameter | Default | Description |
| ----------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graphql-config.load.baseDir` | workspace root or process.cwd() | the path where graphql config looks for config files |
| `graphql-config.load.filePath` | `null` | exact filepath of the config file. |
| `graphql-config.load.configName` | `graphql` | config name prefix instead of `graphql` |
| `graphql-config.load.legacy` | `true` | backwards compatibility with `graphql-config@2` |
| `graphql-config.dotEnvPath` | `null` | backwards compatibility with `graphql-config@2` |
| `vscode-graphql.cacheSchemaFileForLookup` | `false` | generate an SDL file based on your graphql-config schema configuration for schema definition lookup and other features. useful when your `schema` config are urls |

all the `graphql-config.load.*` configuration values come from static `loadConfig()` options in graphql config.

Expand Down
79 changes: 45 additions & 34 deletions packages/graphql-language-service-server/src/MessageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,13 @@ export class MessageProcessor {
contents = cachedDocument.contents;
}
}
if (this._isInitialized && this._graphQLCache) {
const project = this._graphQLCache.getProjectForFile(uri);

if (
this._isInitialized &&
this._graphQLCache &&
project.extensions?.languageService?.enableValidation !== false
) {
await Promise.all(
contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(
Expand All @@ -316,7 +322,6 @@ export class MessageProcessor {
}
}),
);
const project = this._graphQLCache.getProjectForFile(uri);

this._logger.log(
JSON.stringify({
Expand Down Expand Up @@ -375,22 +380,26 @@ export class MessageProcessor {
await this._updateFragmentDefinition(uri, contents);
await this._updateObjectTypeDefinition(uri, contents);

// Send the diagnostics onChange as well
const project = this._graphQLCache.getProjectForFile(uri);
const diagnostics: Diagnostic[] = [];
await Promise.all(
contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(
query,
uri,
this._isRelayCompatMode(query),
);
if (results && results.length > 0) {
diagnostics.push(...processDiagnosticsMessage(results, query, range));
}
}),
);

const project = this._graphQLCache.getProjectForFile(uri);
if (project.extensions?.languageService?.enableValidation !== false) {
// Send the diagnostics onChange as well
await Promise.all(
contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(
query,
uri,
this._isRelayCompatMode(query),
);
if (results && results.length > 0) {
diagnostics.push(
...processDiagnosticsMessage(results, query, range),
);
}
}),
);
}

this._logger.log(
JSON.stringify({
Expand Down Expand Up @@ -593,24 +602,27 @@ export class MessageProcessor {
await this._updateFragmentDefinition(uri, contents);
await this._updateObjectTypeDefinition(uri, contents);

const diagnostics = (
await Promise.all(
contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(
query,
uri,
this._isRelayCompatMode(query),
);
if (results && results.length > 0) {
return processDiagnosticsMessage(results, query, range);
} else {
return [];
}
}),
)
).reduce((left, right) => left.concat(right), []);

const project = this._graphQLCache.getProjectForFile(uri);
let diagnostics: Diagnostic[] = [];

if (project.extensions?.languageService?.enableValidation !== false) {
diagnostics = (
await Promise.all(
contents.map(async ({ query, range }) => {
const results = await this._languageService.getDiagnostics(
query,
uri,
this._isRelayCompatMode(query),
);
if (results && results.length > 0) {
return processDiagnosticsMessage(results, query, range);
} else {
return [];
}
}),
)
).reduce((left, right) => left.concat(right), diagnostics);
}

this._logger.log(
JSON.stringify({
Expand All @@ -620,7 +632,6 @@ export class MessageProcessor {
fileName: uri,
}),
);

return { uri, diagnostics };
} else if (change.type === FileChangeTypeKind.Deleted) {
this._graphQLCache.updateFragmentDefinitionCache(
Expand Down

0 comments on commit d51fe75

Please sign in to comment.