From ecd29f88eadd0ff38ee4ae5c40a64514ec4228c9 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 23 Aug 2023 18:12:53 +0900 Subject: [PATCH 1/9] feat: support queries for multiple schemas in the same file --- .../src/GraphQLCache.ts | 5 +- .../src/GraphQLLanguageService.ts | 47 +++- .../src/MessageProcessor.ts | 217 ++++++++++-------- .../src/__tests__/MessageProcessor-test.ts | 6 + .../src/findGraphQLTags.ts | 3 +- 5 files changed, 165 insertions(+), 113 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index fcf578d42fd..f12296dd10a 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -48,14 +48,15 @@ import glob from 'glob'; import { LoadConfigOptions } from './types'; import { URI } from 'vscode-uri'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; +import { EXTENSION_NAME } from './GraphQLLanguageService'; -const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { +export const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { // For schema api.loaders.schema.register(new CodeFileLoader()); // For documents api.loaders.documents.register(new CodeFileLoader()); - return { name: 'languageService' }; + return { name: EXTENSION_NAME }; }; // Maximum files to read when processing GraphQL files. diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts index aeaa4c92a8e..6b36f44c8da 100644 --- a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -56,6 +56,10 @@ import { SymbolKind, } from 'vscode-languageserver-types'; +import { fileURLToPath } from 'node:url'; + +export const EXTENSION_NAME = 'languageService'; + const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = { [Kind.FIELD]: SymbolKind.Field, [Kind.OPERATION_DEFINITION]: SymbolKind.Class, @@ -96,11 +100,38 @@ export class GraphQLLanguageService { this._logger = logger; } - getConfigForURI(uri: Uri) { - const config = this._graphQLCache.getProjectForFile(uri); - if (config) { - return config; + getAllProjectsForFile(uri: Uri) { + const filePath = uri.startsWith('file:') ? fileURLToPath(uri) : uri; + const projects = Object.values(this._graphQLConfig.projects).filter( + project => project.match(filePath), + ); + + return projects.length > 0 + ? projects + : // Fallback, this always finds at least 1 project + [this._graphQLConfig.getProjectForFile(filePath)]; + } + + getProjectForQuery( + query: string, + uri: Uri, + projects?: GraphQLProjectConfig[], + ) { + if (!query.startsWith('#graphql')) { + // Query is not annotated with #graphql. + // Skip suffix check and return the first project that matches the file. + return projects?.[0] ?? this._graphQLConfig.getProjectForFile(uri); } + + return (projects || this.getAllProjectsForFile(uri)).find(project => { + const ext = project.hasExtension(EXTENSION_NAME) + ? project.extension(EXTENSION_NAME) + : null; + + const suffix = ext?.gqlTagOptions?.annotationSuffix; + + return query.startsWith(`#graphql${suffix ? ':' + suffix : ''}\n`); + }); } public async getDiagnostics( @@ -111,7 +142,7 @@ export class GraphQLLanguageService { // Perform syntax diagnostics first, as this doesn't require // schema/fragment definitions, even the project configuration. let documentHasExtensions = false; - const projectConfig = this.getConfigForURI(uri); + const projectConfig = this.getProjectForQuery(document, uri); // skip validation when there's nothing to validate, prevents noisy unexpected EOF errors if (!projectConfig || !document || document.trim().length < 2) { return []; @@ -218,7 +249,7 @@ export class GraphQLLanguageService { position: IPosition, filePath: Uri, ): Promise> { - const projectConfig = this.getConfigForURI(filePath); + const projectConfig = this.getProjectForQuery(query, filePath); if (!projectConfig) { return []; } @@ -255,7 +286,7 @@ export class GraphQLLanguageService { filePath: Uri, options?: HoverConfig, ): Promise { - const projectConfig = this.getConfigForURI(filePath); + const projectConfig = this.getProjectForQuery(query, filePath); if (!projectConfig) { return ''; } @@ -272,7 +303,7 @@ export class GraphQLLanguageService { position: IPosition, filePath: Uri, ): Promise { - const projectConfig = this.getConfigForURI(filePath); + const projectConfig = this.getProjectForQuery(query, filePath); if (!projectConfig) { return null; } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 007d78f862f..baf6d515436 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -288,6 +288,39 @@ export class MessageProcessor { ); } + async _getDiagnosticsForAllFileProjects( + contents: CachedContent[], + uri: Uri, + projects: GraphQLProjectConfig[], + ): Promise { + return ( + await Promise.all( + contents.map(async ({ query, range }) => { + const project = this._languageService.getProjectForQuery( + query, + uri, + projects, + ); + + if ( + project?.extensions?.languageService?.enableValidation !== false + ) { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + return processDiagnosticsMessage(results, query, range); + } + } + + return []; + }), + ) + ).reduce((left, right) => left.concat(right), []); + } + async handleDidOpenOrSaveNotification( params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, ): Promise { @@ -359,35 +392,28 @@ export class MessageProcessor { return { uri, diagnostics }; } try { - const project = this._graphQLCache.getProjectForFile(uri); - if ( - this._isInitialized && - project?.extensions?.languageService?.enableValidation !== false - ) { - 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 projects = this._languageService.getAllProjectsForFile(uri); + + if (this._isInitialized) { + diagnostics.push( + ...(await this._getDiagnosticsForAllFileProjects( + contents, + uri, + projects, + )), ); } - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didOpenOrSave', - projectName: project?.name, - fileName: uri, - }), - ); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didOpenOrSave', + projectName: project?.name, + fileName: uri, + }), + ); + } } catch (err) { this._handleConfigError({ err, uri }); } @@ -416,7 +442,6 @@ export class MessageProcessor { } const { textDocument, contentChanges } = params; const { uri } = textDocument; - const project = this._graphQLCache.getProjectForFile(uri); try { const contentChange = contentChanges.at(-1)!; @@ -438,35 +463,25 @@ export class MessageProcessor { await this._updateFragmentDefinition(uri, contents); await this._updateObjectTypeDefinition(uri, contents); - const diagnostics: Diagnostic[] = []; + const projects = this._languageService.getAllProjectsForFile(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), - ); - } + const diagnostics = await this._getDiagnosticsForAllFileProjects( + contents, + uri, + projects, + ); + + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didChange', + projectName: project?.name, + fileName: uri, }), ); } - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didChange', - projectName: project?.name, - fileName: uri, - }), - ); - return { uri, diagnostics }; } catch (err) { this._handleConfigError({ err, uri }); @@ -502,16 +517,18 @@ export class MessageProcessor { if (this._textDocumentCache.has(uri)) { this._textDocumentCache.delete(uri); } - const project = this._graphQLCache.getProjectForFile(uri); + const projects = this._languageService.getAllProjectsForFile(uri); - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didClose', - projectName: project?.name, - fileName: uri, - }), - ); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didClose', + projectName: project?.name, + fileName: uri, + }), + ); + } } handleShutdownRequest(): void { @@ -574,7 +591,10 @@ export class MessageProcessor { textDocument.uri, ); - const project = this._graphQLCache.getProjectForFile(textDocument.uri); + const project = this._languageService.getProjectForQuery( + textDocument.uri, + query, + ); this._logger.log( JSON.stringify({ @@ -664,39 +684,28 @@ export class MessageProcessor { await this._updateFragmentDefinition(uri, contents); await this._updateObjectTypeDefinition(uri, contents); - const project = this._graphQLCache.getProjectForFile(uri); - await this._updateSchemaIfChanged(project, uri); + const projects = this._languageService.getAllProjectsForFile(uri); + await Promise.all( + projects.map(project => this._updateSchemaIfChanged(project, uri)), + ); - let diagnostics: Diagnostic[] = []; + const diagnostics = await this._getDiagnosticsForAllFileProjects( + contents, + uri, + projects, + ); - 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); - } - return []; - }), - ) - ).reduce((left, right) => left.concat(right), diagnostics); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'workspace/didChangeWatchedFiles', + projectName: project?.name, + fileName: uri, + }), + ); } - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'workspace/didChangeWatchedFiles', - projectName: project?.name, - fileName: uri, - }), - ); return { uri, diagnostics }; } if (change.type === FileChangeTypeKind.Deleted) { @@ -727,9 +736,13 @@ export class MessageProcessor { throw new Error('`textDocument` and `position` arguments are required.'); } const { textDocument, position } = params; - const project = this._graphQLCache.getProjectForFile(textDocument.uri); - if (project) { - await this._cacheSchemaFilesForProject(project); + const projects = this._languageService.getAllProjectsForFile( + textDocument.uri, + ); + if (projects.length > 0) { + await Promise.all( + projects.map(project => this._cacheSchemaFilesForProject(project)), + ); } const cachedDocument = this._getCachedDocument(textDocument.uri); if (!cachedDocument) { @@ -802,14 +815,16 @@ export class MessageProcessor { }) : []; - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/definition', - projectName: project?.name, - fileName: textDocument.uri, - }), - ); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/definition', + projectName: project?.name, + fileName: textDocument.uri, + }), + ); + } return formatted; } diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index 16b639bb4f6..b1f988a6903 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -66,6 +66,12 @@ describe('MessageProcessor', () => { getDiagnostics(_query, _uri) { return []; }, + getAllProjectsForFile(uri: string) { + return [messageProcessor._graphQLCache.getProjectForFile(uri)]; + }, + getProjectForQuery(_query: string, uri: string) { + return this.getAllProjectsForFile(uri)[0]; + }, async getDocumentSymbols(_query: string, uri: string) { return [ { diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.ts b/packages/graphql-language-service-server/src/findGraphQLTags.ts index a1f0a64830d..ea4678be420 100644 --- a/packages/graphql-language-service-server/src/findGraphQLTags.ts +++ b/packages/graphql-language-service-server/src/findGraphQLTags.ts @@ -254,8 +254,7 @@ export function findGraphQLTags( } }, TemplateLiteral(node: TemplateLiteral) { - const hasGraphQLPrefix = - node.quasis[0].value.raw.startsWith('#graphql\n'); + const hasGraphQLPrefix = node.quasis[0].value.raw.startsWith('#graphql'); const hasGraphQLComment = Boolean( node.leadingComments?.[0]?.value.match(/^\s*GraphQL\s*$/), ); From b20c7edd02f7eee3d0f34047007e9d9751cc3bbf Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 23 Aug 2023 18:21:40 +0900 Subject: [PATCH 2/9] Test queries for multiple schemas in the same file --- .../__tests__/GraphQLLanguageService-test.ts | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts index 0283c277174..faa9d4d363f 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts @@ -10,7 +10,10 @@ import { join } from 'node:path'; import { GraphQLConfig } from 'graphql-config'; -import { GraphQLLanguageService } from '../GraphQLLanguageService'; +import { + EXTENSION_NAME, + GraphQLLanguageService, +} from '../GraphQLLanguageService'; import { SymbolKind } from 'vscode-languageserver-protocol'; import { Position } from 'graphql-language-service'; import { NoopLogger } from '../Logger'; @@ -18,8 +21,23 @@ import { NoopLogger } from '../Logger'; const MOCK_CONFIG = { filepath: join(__dirname, '.graphqlrc.yml'), config: { - schema: './__schema__/StarWarsSchema.graphql', - documents: ['./queries/**', '**/*.graphql'], + projects: { + default: { + schema: './__schema__/StarWarsSchema.graphql', + documents: ['./queries/**', '**/*.graphql'], + }, + another: { + schema: 'schema { query: Query } type Query { test: String }', + documents: ['./queries/**/*.ts', './somewhere/**/*.ts'], + extensions: { + [EXTENSION_NAME]: { + gqlTagOptions: { + annotationSuffix: 'test', + }, + }, + }, + }, + }, }, }; @@ -31,7 +49,7 @@ describe('GraphQLLanguageService', () => { }, getGraphQLConfig() { - return new GraphQLConfig(MOCK_CONFIG, []); + return new GraphQLConfig(MOCK_CONFIG, [() => ({ name: EXTENSION_NAME })]); }, getProjectForFile(uri: string) { @@ -221,4 +239,35 @@ describe('GraphQLLanguageService', () => { expect(result[1].location.range.end.line).toEqual(4); expect(result[1].location.range.end.character).toEqual(5); }); + + it('finds the correct project for the given query', () => { + const getProjectName = (query: string, path: string) => + languageService.getProjectForQuery(query, path)?.name; + + const QUERY_NO_SUFFIX = '#graphql\n query { test }'; + const QUERY_TEST_SUFFIX = '#graphql:test\n query { test }'; + + const pathThatMatchesBothProjects = './queries/test.ts'; + const pathThatMatchesOnlyProjectAnother = './somewhere/test.ts'; + + // Matches path for both projects: + // #graphql => default + expect( + getProjectName(QUERY_NO_SUFFIX, pathThatMatchesBothProjects), + ).toEqual('default'); + // #graphql:test => another + expect( + getProjectName(QUERY_TEST_SUFFIX, pathThatMatchesBothProjects), + ).toEqual('another'); + + // Only matches path for project 'another': + // #graphql => undefined + expect( + getProjectName(QUERY_NO_SUFFIX, pathThatMatchesOnlyProjectAnother), + ).toEqual(undefined); + // #graphql:test => another + expect( + getProjectName(QUERY_TEST_SUFFIX, pathThatMatchesOnlyProjectAnother), + ).toEqual('another'); + }); }); From ffca1f75bce2afd147a4b1477956834b777989f1 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 23 Aug 2023 18:12:53 +0900 Subject: [PATCH 3/9] feat: support queries for multiple schemas in the same file --- .../src/GraphQLCache.ts | 5 +- .../src/GraphQLLanguageService.ts | 47 +++- .../src/MessageProcessor.ts | 217 ++++++++++-------- .../src/__tests__/MessageProcessor-test.ts | 6 + .../src/findGraphQLTags.ts | 3 +- 5 files changed, 165 insertions(+), 113 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index fcf578d42fd..f12296dd10a 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -48,14 +48,15 @@ import glob from 'glob'; import { LoadConfigOptions } from './types'; import { URI } from 'vscode-uri'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; +import { EXTENSION_NAME } from './GraphQLLanguageService'; -const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { +export const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { // For schema api.loaders.schema.register(new CodeFileLoader()); // For documents api.loaders.documents.register(new CodeFileLoader()); - return { name: 'languageService' }; + return { name: EXTENSION_NAME }; }; // Maximum files to read when processing GraphQL files. diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts index aeaa4c92a8e..6b36f44c8da 100644 --- a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -56,6 +56,10 @@ import { SymbolKind, } from 'vscode-languageserver-types'; +import { fileURLToPath } from 'node:url'; + +export const EXTENSION_NAME = 'languageService'; + const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = { [Kind.FIELD]: SymbolKind.Field, [Kind.OPERATION_DEFINITION]: SymbolKind.Class, @@ -96,11 +100,38 @@ export class GraphQLLanguageService { this._logger = logger; } - getConfigForURI(uri: Uri) { - const config = this._graphQLCache.getProjectForFile(uri); - if (config) { - return config; + getAllProjectsForFile(uri: Uri) { + const filePath = uri.startsWith('file:') ? fileURLToPath(uri) : uri; + const projects = Object.values(this._graphQLConfig.projects).filter( + project => project.match(filePath), + ); + + return projects.length > 0 + ? projects + : // Fallback, this always finds at least 1 project + [this._graphQLConfig.getProjectForFile(filePath)]; + } + + getProjectForQuery( + query: string, + uri: Uri, + projects?: GraphQLProjectConfig[], + ) { + if (!query.startsWith('#graphql')) { + // Query is not annotated with #graphql. + // Skip suffix check and return the first project that matches the file. + return projects?.[0] ?? this._graphQLConfig.getProjectForFile(uri); } + + return (projects || this.getAllProjectsForFile(uri)).find(project => { + const ext = project.hasExtension(EXTENSION_NAME) + ? project.extension(EXTENSION_NAME) + : null; + + const suffix = ext?.gqlTagOptions?.annotationSuffix; + + return query.startsWith(`#graphql${suffix ? ':' + suffix : ''}\n`); + }); } public async getDiagnostics( @@ -111,7 +142,7 @@ export class GraphQLLanguageService { // Perform syntax diagnostics first, as this doesn't require // schema/fragment definitions, even the project configuration. let documentHasExtensions = false; - const projectConfig = this.getConfigForURI(uri); + const projectConfig = this.getProjectForQuery(document, uri); // skip validation when there's nothing to validate, prevents noisy unexpected EOF errors if (!projectConfig || !document || document.trim().length < 2) { return []; @@ -218,7 +249,7 @@ export class GraphQLLanguageService { position: IPosition, filePath: Uri, ): Promise> { - const projectConfig = this.getConfigForURI(filePath); + const projectConfig = this.getProjectForQuery(query, filePath); if (!projectConfig) { return []; } @@ -255,7 +286,7 @@ export class GraphQLLanguageService { filePath: Uri, options?: HoverConfig, ): Promise { - const projectConfig = this.getConfigForURI(filePath); + const projectConfig = this.getProjectForQuery(query, filePath); if (!projectConfig) { return ''; } @@ -272,7 +303,7 @@ export class GraphQLLanguageService { position: IPosition, filePath: Uri, ): Promise { - const projectConfig = this.getConfigForURI(filePath); + const projectConfig = this.getProjectForQuery(query, filePath); if (!projectConfig) { return null; } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 007d78f862f..baf6d515436 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -288,6 +288,39 @@ export class MessageProcessor { ); } + async _getDiagnosticsForAllFileProjects( + contents: CachedContent[], + uri: Uri, + projects: GraphQLProjectConfig[], + ): Promise { + return ( + await Promise.all( + contents.map(async ({ query, range }) => { + const project = this._languageService.getProjectForQuery( + query, + uri, + projects, + ); + + if ( + project?.extensions?.languageService?.enableValidation !== false + ) { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + return processDiagnosticsMessage(results, query, range); + } + } + + return []; + }), + ) + ).reduce((left, right) => left.concat(right), []); + } + async handleDidOpenOrSaveNotification( params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, ): Promise { @@ -359,35 +392,28 @@ export class MessageProcessor { return { uri, diagnostics }; } try { - const project = this._graphQLCache.getProjectForFile(uri); - if ( - this._isInitialized && - project?.extensions?.languageService?.enableValidation !== false - ) { - 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 projects = this._languageService.getAllProjectsForFile(uri); + + if (this._isInitialized) { + diagnostics.push( + ...(await this._getDiagnosticsForAllFileProjects( + contents, + uri, + projects, + )), ); } - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didOpenOrSave', - projectName: project?.name, - fileName: uri, - }), - ); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didOpenOrSave', + projectName: project?.name, + fileName: uri, + }), + ); + } } catch (err) { this._handleConfigError({ err, uri }); } @@ -416,7 +442,6 @@ export class MessageProcessor { } const { textDocument, contentChanges } = params; const { uri } = textDocument; - const project = this._graphQLCache.getProjectForFile(uri); try { const contentChange = contentChanges.at(-1)!; @@ -438,35 +463,25 @@ export class MessageProcessor { await this._updateFragmentDefinition(uri, contents); await this._updateObjectTypeDefinition(uri, contents); - const diagnostics: Diagnostic[] = []; + const projects = this._languageService.getAllProjectsForFile(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), - ); - } + const diagnostics = await this._getDiagnosticsForAllFileProjects( + contents, + uri, + projects, + ); + + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didChange', + projectName: project?.name, + fileName: uri, }), ); } - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didChange', - projectName: project?.name, - fileName: uri, - }), - ); - return { uri, diagnostics }; } catch (err) { this._handleConfigError({ err, uri }); @@ -502,16 +517,18 @@ export class MessageProcessor { if (this._textDocumentCache.has(uri)) { this._textDocumentCache.delete(uri); } - const project = this._graphQLCache.getProjectForFile(uri); + const projects = this._languageService.getAllProjectsForFile(uri); - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didClose', - projectName: project?.name, - fileName: uri, - }), - ); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didClose', + projectName: project?.name, + fileName: uri, + }), + ); + } } handleShutdownRequest(): void { @@ -574,7 +591,10 @@ export class MessageProcessor { textDocument.uri, ); - const project = this._graphQLCache.getProjectForFile(textDocument.uri); + const project = this._languageService.getProjectForQuery( + textDocument.uri, + query, + ); this._logger.log( JSON.stringify({ @@ -664,39 +684,28 @@ export class MessageProcessor { await this._updateFragmentDefinition(uri, contents); await this._updateObjectTypeDefinition(uri, contents); - const project = this._graphQLCache.getProjectForFile(uri); - await this._updateSchemaIfChanged(project, uri); + const projects = this._languageService.getAllProjectsForFile(uri); + await Promise.all( + projects.map(project => this._updateSchemaIfChanged(project, uri)), + ); - let diagnostics: Diagnostic[] = []; + const diagnostics = await this._getDiagnosticsForAllFileProjects( + contents, + uri, + projects, + ); - 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); - } - return []; - }), - ) - ).reduce((left, right) => left.concat(right), diagnostics); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'workspace/didChangeWatchedFiles', + projectName: project?.name, + fileName: uri, + }), + ); } - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'workspace/didChangeWatchedFiles', - projectName: project?.name, - fileName: uri, - }), - ); return { uri, diagnostics }; } if (change.type === FileChangeTypeKind.Deleted) { @@ -727,9 +736,13 @@ export class MessageProcessor { throw new Error('`textDocument` and `position` arguments are required.'); } const { textDocument, position } = params; - const project = this._graphQLCache.getProjectForFile(textDocument.uri); - if (project) { - await this._cacheSchemaFilesForProject(project); + const projects = this._languageService.getAllProjectsForFile( + textDocument.uri, + ); + if (projects.length > 0) { + await Promise.all( + projects.map(project => this._cacheSchemaFilesForProject(project)), + ); } const cachedDocument = this._getCachedDocument(textDocument.uri); if (!cachedDocument) { @@ -802,14 +815,16 @@ export class MessageProcessor { }) : []; - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/definition', - projectName: project?.name, - fileName: textDocument.uri, - }), - ); + for (const project of projects) { + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/definition', + projectName: project?.name, + fileName: textDocument.uri, + }), + ); + } return formatted; } diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index 16b639bb4f6..b1f988a6903 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -66,6 +66,12 @@ describe('MessageProcessor', () => { getDiagnostics(_query, _uri) { return []; }, + getAllProjectsForFile(uri: string) { + return [messageProcessor._graphQLCache.getProjectForFile(uri)]; + }, + getProjectForQuery(_query: string, uri: string) { + return this.getAllProjectsForFile(uri)[0]; + }, async getDocumentSymbols(_query: string, uri: string) { return [ { diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.ts b/packages/graphql-language-service-server/src/findGraphQLTags.ts index a1f0a64830d..ea4678be420 100644 --- a/packages/graphql-language-service-server/src/findGraphQLTags.ts +++ b/packages/graphql-language-service-server/src/findGraphQLTags.ts @@ -254,8 +254,7 @@ export function findGraphQLTags( } }, TemplateLiteral(node: TemplateLiteral) { - const hasGraphQLPrefix = - node.quasis[0].value.raw.startsWith('#graphql\n'); + const hasGraphQLPrefix = node.quasis[0].value.raw.startsWith('#graphql'); const hasGraphQLComment = Boolean( node.leadingComments?.[0]?.value.match(/^\s*GraphQL\s*$/), ); From bbc8a6e4668f1f6e839c64b1beb830bfe387afe4 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Wed, 23 Aug 2023 18:21:40 +0900 Subject: [PATCH 4/9] Test queries for multiple schemas in the same file --- .../__tests__/GraphQLLanguageService-test.ts | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts index 0283c277174..faa9d4d363f 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts @@ -10,7 +10,10 @@ import { join } from 'node:path'; import { GraphQLConfig } from 'graphql-config'; -import { GraphQLLanguageService } from '../GraphQLLanguageService'; +import { + EXTENSION_NAME, + GraphQLLanguageService, +} from '../GraphQLLanguageService'; import { SymbolKind } from 'vscode-languageserver-protocol'; import { Position } from 'graphql-language-service'; import { NoopLogger } from '../Logger'; @@ -18,8 +21,23 @@ import { NoopLogger } from '../Logger'; const MOCK_CONFIG = { filepath: join(__dirname, '.graphqlrc.yml'), config: { - schema: './__schema__/StarWarsSchema.graphql', - documents: ['./queries/**', '**/*.graphql'], + projects: { + default: { + schema: './__schema__/StarWarsSchema.graphql', + documents: ['./queries/**', '**/*.graphql'], + }, + another: { + schema: 'schema { query: Query } type Query { test: String }', + documents: ['./queries/**/*.ts', './somewhere/**/*.ts'], + extensions: { + [EXTENSION_NAME]: { + gqlTagOptions: { + annotationSuffix: 'test', + }, + }, + }, + }, + }, }, }; @@ -31,7 +49,7 @@ describe('GraphQLLanguageService', () => { }, getGraphQLConfig() { - return new GraphQLConfig(MOCK_CONFIG, []); + return new GraphQLConfig(MOCK_CONFIG, [() => ({ name: EXTENSION_NAME })]); }, getProjectForFile(uri: string) { @@ -221,4 +239,35 @@ describe('GraphQLLanguageService', () => { expect(result[1].location.range.end.line).toEqual(4); expect(result[1].location.range.end.character).toEqual(5); }); + + it('finds the correct project for the given query', () => { + const getProjectName = (query: string, path: string) => + languageService.getProjectForQuery(query, path)?.name; + + const QUERY_NO_SUFFIX = '#graphql\n query { test }'; + const QUERY_TEST_SUFFIX = '#graphql:test\n query { test }'; + + const pathThatMatchesBothProjects = './queries/test.ts'; + const pathThatMatchesOnlyProjectAnother = './somewhere/test.ts'; + + // Matches path for both projects: + // #graphql => default + expect( + getProjectName(QUERY_NO_SUFFIX, pathThatMatchesBothProjects), + ).toEqual('default'); + // #graphql:test => another + expect( + getProjectName(QUERY_TEST_SUFFIX, pathThatMatchesBothProjects), + ).toEqual('another'); + + // Only matches path for project 'another': + // #graphql => undefined + expect( + getProjectName(QUERY_NO_SUFFIX, pathThatMatchesOnlyProjectAnother), + ).toEqual(undefined); + // #graphql:test => another + expect( + getProjectName(QUERY_TEST_SUFFIX, pathThatMatchesOnlyProjectAnother), + ).toEqual('another'); + }); }); From 5dcd471cec3624e5582fa81eca1eaf6375de81ac Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Mon, 11 Dec 2023 14:51:45 +0100 Subject: [PATCH 5/9] Fix uri in non-annotated cases --- .../src/GraphQLLanguageService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts index 6b36f44c8da..18b10ca3550 100644 --- a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -88,6 +88,10 @@ function getKind(tree: OutlineTree) { return KIND_TO_SYMBOL_KIND[tree.kind]; } +function normalizeUri(uri: string) { + return uri.startsWith('file:') ? fileURLToPath(uri) : uri; +} + export class GraphQLLanguageService { _graphQLCache: GraphQLCache; _graphQLConfig: GraphQLConfig; @@ -101,7 +105,7 @@ export class GraphQLLanguageService { } getAllProjectsForFile(uri: Uri) { - const filePath = uri.startsWith('file:') ? fileURLToPath(uri) : uri; + const filePath = normalizeUri(uri); const projects = Object.values(this._graphQLConfig.projects).filter( project => project.match(filePath), ); @@ -120,7 +124,10 @@ export class GraphQLLanguageService { if (!query.startsWith('#graphql')) { // Query is not annotated with #graphql. // Skip suffix check and return the first project that matches the file. - return projects?.[0] ?? this._graphQLConfig.getProjectForFile(uri); + return ( + projects?.[0] ?? + this._graphQLConfig.getProjectForFile(normalizeUri(uri)) + ); } return (projects || this.getAllProjectsForFile(uri)).find(project => { From 12acf3e0c4c84c5920576c6e20de9c00ec876fb0 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Mon, 8 Jan 2024 20:16:04 +0100 Subject: [PATCH 6/9] Add docs to readmes --- .../graphql-language-service-server/README.md | 39 +++++++++++++++---- packages/vscode-graphql/README.md | 39 +++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/packages/graphql-language-service-server/README.md b/packages/graphql-language-service-server/README.md index d0f5388cd9a..2a02c3bce2b 100644 --- a/packages/graphql-language-service-server/README.md +++ b/packages/graphql-language-service-server/README.md @@ -29,6 +29,7 @@ Supported features include: - Support for `gql` `graphql` and other template tags inside javascript, typescript, jsx, ts, vue and svelte files, and an interface to allow custom parsing of all files. +- Support multiple GraphQL APIs in the same file via annotation suffixes. ## Installation and Usage @@ -187,6 +188,9 @@ export default { languageService: { cacheSchemaFileForLookup: true, enableValidation: false, + gqlTagOptions: { + annotationSuffix: 'my-project', + }, }, }, }, @@ -237,18 +241,37 @@ 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 | +| 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 | +| `vscode-graphql.gqlTagOptions.annotationSuffix` | `null` | establish a suffix to match queries to a project schema using `#graphql:` comment. Only the first matching project for a given query is used, thus supporting multiple queries for different schemas in the same file | all the `graphql-config.load.*` configuration values come from static `loadConfig()` options in graphql config. +Use the `gqlTagOptions.annotationSuffix` option to mix queries for different schemas in the same file. Each query annotated with the `#graphql:` comment will be matched to the first project with the same suffix: + +```ts +// file.js + +const queryForDefaultProject = `#graphql + query { something } +`; + +const queryForDbProject = `#graphql:db + query { something } +`; + +const queryForCmsProject = `#graphql:cms + query { something } +`; +``` + (more coming soon!) ### Architectural Overview diff --git a/packages/vscode-graphql/README.md b/packages/vscode-graphql/README.md index 6cad22385da..dca37185a37 100644 --- a/packages/vscode-graphql/README.md +++ b/packages/vscode-graphql/README.md @@ -128,6 +128,11 @@ module.exports = { }, }, ], + languageService: { + gqlTagOptions: { + annotationSuffix: 'db', + }, + }, }, }, }, @@ -137,6 +142,24 @@ module.exports = { Notice that `documents` key supports glob pattern and hence `["**/*.graphql"]` is also valid. +Normally, you want to point your `documents` to different files per project to ensure only one schema is used for the queries. However, you can also mix queries for different schemas in the same file by adding a `#graphql:` comment to the query that matches the `languageService.gqlTagOptions.annotationSuffix` for the project: + +```ts +// file.js + +const queryForDefaultProject = `#graphql + query { something } +`; + +const queryForDbProject = `#graphql:db + query { something } +`; + +const queryForCmsProject = `#graphql:cms + query { something } +`; +``` + ## Frequently Asked Questions @@ -300,14 +323,14 @@ further! This plugin uses the [GraphQL language server](https://github.com/graphql/graphql-language-service-server) -1. Clone the repository - https://github.com/graphql/graphiql -1. `yarn` -1. Run "VScode Extension" launcher in vscode -1. This will open another VSCode instance with extension enabled -1. Open a project with a graphql config file - ":electric_plug: graphql" in - VSCode status bar indicates that the extension is in use -1. Logs for GraphQL language service will appear in output section under - GraphQL Language Service +1. Clone the repository - +1. `yarn` +1. Run "VScode Extension" launcher in vscode +1. This will open another VSCode instance with extension enabled +1. Open a project with a graphql config file - ":electric_plug: graphql" in + VSCode status bar indicates that the extension is in use +1. Logs for GraphQL language service will appear in output section under + GraphQL Language Service ### Contributing back to this project From 935ae8a2cf5c7406d3e8f2c3f70acb0f3fcf1c54 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Thu, 25 Jan 2024 16:35:29 +0900 Subject: [PATCH 7/9] Rename function --- .../src/GraphQLLanguageService.ts | 16 ++++++++-------- .../src/MessageProcessor.ts | 4 ++-- .../src/__tests__/GraphQLLanguageService-test.ts | 2 +- .../src/__tests__/MessageProcessor-test.ts | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts index 18b10ca3550..129dbbb7d7f 100644 --- a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -116,12 +116,12 @@ export class GraphQLLanguageService { [this._graphQLConfig.getProjectForFile(filePath)]; } - getProjectForQuery( - query: string, + getProjectForDocument( + document: string, uri: Uri, projects?: GraphQLProjectConfig[], ) { - if (!query.startsWith('#graphql')) { + if (!document.startsWith('#graphql')) { // Query is not annotated with #graphql. // Skip suffix check and return the first project that matches the file. return ( @@ -137,7 +137,7 @@ export class GraphQLLanguageService { const suffix = ext?.gqlTagOptions?.annotationSuffix; - return query.startsWith(`#graphql${suffix ? ':' + suffix : ''}\n`); + return document.startsWith(`#graphql${suffix ? ':' + suffix : ''}\n`); }); } @@ -149,7 +149,7 @@ export class GraphQLLanguageService { // Perform syntax diagnostics first, as this doesn't require // schema/fragment definitions, even the project configuration. let documentHasExtensions = false; - const projectConfig = this.getProjectForQuery(document, uri); + const projectConfig = this.getProjectForDocument(document, uri); // skip validation when there's nothing to validate, prevents noisy unexpected EOF errors if (!projectConfig || !document || document.trim().length < 2) { return []; @@ -256,7 +256,7 @@ export class GraphQLLanguageService { position: IPosition, filePath: Uri, ): Promise> { - const projectConfig = this.getProjectForQuery(query, filePath); + const projectConfig = this.getProjectForDocument(query, filePath); if (!projectConfig) { return []; } @@ -293,7 +293,7 @@ export class GraphQLLanguageService { filePath: Uri, options?: HoverConfig, ): Promise { - const projectConfig = this.getProjectForQuery(query, filePath); + const projectConfig = this.getProjectForDocument(query, filePath); if (!projectConfig) { return ''; } @@ -310,7 +310,7 @@ export class GraphQLLanguageService { position: IPosition, filePath: Uri, ): Promise { - const projectConfig = this.getProjectForQuery(query, filePath); + const projectConfig = this.getProjectForDocument(query, filePath); if (!projectConfig) { return null; } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 0c4101f6821..a1e87a3e705 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -300,7 +300,7 @@ export class MessageProcessor { return ( await Promise.all( contents.map(async ({ query, range }) => { - const project = this._languageService.getProjectForQuery( + const project = this._languageService.getProjectForDocument( query, uri, projects, @@ -595,7 +595,7 @@ export class MessageProcessor { textDocument.uri, ); - const project = this._languageService.getProjectForQuery( + const project = this._languageService.getProjectForDocument( textDocument.uri, query, ); diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts index faa9d4d363f..962b7c5968a 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts @@ -242,7 +242,7 @@ describe('GraphQLLanguageService', () => { it('finds the correct project for the given query', () => { const getProjectName = (query: string, path: string) => - languageService.getProjectForQuery(query, path)?.name; + languageService.getProjectForDocument(query, path)?.name; const QUERY_NO_SUFFIX = '#graphql\n query { test }'; const QUERY_TEST_SUFFIX = '#graphql:test\n query { test }'; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index 9a55aa1a148..e43061ceaeb 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -69,7 +69,7 @@ describe('MessageProcessor', () => { getAllProjectsForFile(uri: string) { return [messageProcessor._graphQLCache.getProjectForFile(uri)]; }, - getProjectForQuery(_query: string, uri: string) { + getProjectForDocument(_query: string, uri: string) { return this.getAllProjectsForFile(uri)[0]; }, async getDocumentSymbols(_query: string, uri: string) { From 6b9582cfeae0145cbe051c06020e9821a5f64c84 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Thu, 25 Jan 2024 16:37:28 +0900 Subject: [PATCH 8/9] Fix type in related test --- .../src/__tests__/MessageProcessor-test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts index e43061ceaeb..03947d08592 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -67,7 +67,8 @@ describe('MessageProcessor', () => { return []; }, getAllProjectsForFile(uri: string) { - return [messageProcessor._graphQLCache.getProjectForFile(uri)]; + const project = messageProcessor._graphQLCache.getProjectForFile(uri); + return project ? [project] : []; }, getProjectForDocument(_query: string, uri: string) { return this.getAllProjectsForFile(uri)[0]; From b956407a9621b61128bc29321e68420199b3f758 Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Thu, 25 Jan 2024 16:42:52 +0900 Subject: [PATCH 9/9] Update doc with partial feedback --- packages/vscode-graphql/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-graphql/README.md b/packages/vscode-graphql/README.md index dca37185a37..09b9280d6d3 100644 --- a/packages/vscode-graphql/README.md +++ b/packages/vscode-graphql/README.md @@ -142,7 +142,7 @@ module.exports = { Notice that `documents` key supports glob pattern and hence `["**/*.graphql"]` is also valid. -Normally, you want to point your `documents` to different files per project to ensure only one schema is used for the queries. However, you can also mix queries for different schemas in the same file by adding a `#graphql:` comment to the query that matches the `languageService.gqlTagOptions.annotationSuffix` for the project: +Normally, you would point your `documents` in each project to different files to ensure that only one schema is used for the queries. However, you can also mix queries for different schemas into the same file by adding a `#graphql:` comment to each query, matching the `languageService.gqlTagOptions.annotationSuffix` for the project: ```ts // file.js