From 65bcc4dfea606bb61b6c28d58124dcd128b88230 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 16:13:58 +0100 Subject: [PATCH 01/49] =?UTF-8?q?test=20after=20coverage=20for=20recent=20?= =?UTF-8?q?features/changes=20=F0=9F=98=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/rotten-seahorses-fry.md | 6 ++ .../package.json | 5 +- .../src/MessageProcessor.ts | 8 ++- .../src/__tests__/MessageProcessor-test.ts | 16 +++++ .../src/__tests__/findGraphQLTags-test.ts | 32 ++++++++++ .../tests/__fixtures__/test.astro | 30 +++++++++ .../__snapshots__/js-grammar.spec.ts.snap | 64 +++++++++++++++++++ .../tests/js-grammar.spec.ts | 4 ++ yarn.lock | 64 +++++++++++++++++-- 9 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 .changeset/rotten-seahorses-fry.md create mode 100644 packages/vscode-graphql-syntax/tests/__fixtures__/test.astro diff --git a/.changeset/rotten-seahorses-fry.md b/.changeset/rotten-seahorses-fry.md new file mode 100644 index 00000000000..e30441acd17 --- /dev/null +++ b/.changeset/rotten-seahorses-fry.md @@ -0,0 +1,6 @@ +--- +'graphql-language-service-server': patch +'vscode-graphql-syntax': patch +--- + +Fix crash on saving empty package.json file diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json index d4cd4eaceff..17f417f2588 100644 --- a/packages/graphql-language-service-server/package.json +++ b/packages/graphql-language-service-server/package.json @@ -56,15 +56,16 @@ "source-map-js": "1.0.2", "svelte": "^4.1.1", "svelte2tsx": "^0.7.0", + "typescript": "^5.3.3", "vscode-jsonrpc": "^8.0.1", "vscode-languageserver": "^8.0.1", "vscode-languageserver-types": "^3.17.2", - "vscode-uri": "^3.0.2", - "typescript": "^5.3.3" + "vscode-uri": "^3.0.2" }, "devDependencies": { "@types/glob": "^8.1.0", "@types/mkdirp": "^1.0.1", + "@types/mock-fs": "^4.13.4", "cross-env": "^7.0.2", "graphql": "^16.8.1", "mock-fs": "^5.2.0" diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index e871ba4e340..5c9aea744bc 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -9,7 +9,7 @@ import mkdirp from 'mkdirp'; import { readFileSync, existsSync, writeFileSync } from 'node:fs'; -import { writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; import glob from 'fast-glob'; import { URI } from 'vscode-uri'; @@ -308,8 +308,10 @@ export class MessageProcessor { return fileMatch; } if (uri.match('package.json')?.length) { - const graphqlConfig = await import(URI.parse(uri).fsPath); - return Boolean(graphqlConfig?.graphql); + try { + const pkgConfig = await readFile(URI.parse(uri).fsPath, 'utf-8'); + return Boolean(JSON.parse(pkgConfig)?.graphql); + } catch {} } return false; } 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 e2c2ecdaaf9..3f5fe2f4c54 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -151,7 +151,23 @@ describe('MessageProcessor', () => { expect(capabilities.completionProvider.resolveProvider).toEqual(true); expect(capabilities.textDocumentSync).toEqual(1); }); + it('detects a config file', async () => { + const result = await messageProcessor._isGraphQLConfigFile( + 'graphql.config.js', + ); + expect(result).toEqual(true); + const falseResult = await messageProcessor._isGraphQLConfigFile( + 'graphql.js', + ); + expect(falseResult).toEqual(false); + mockfs({ [`${__dirname}/package.json`]: '{"graphql": {}}' }); + const pkgResult = await messageProcessor._isGraphQLConfigFile( + `file://${__dirname}/package.json`, + ); + mockfs.restore(); + expect(pkgResult).toEqual(true); + }); it('runs completion requests properly', async () => { const uri = `${queryPathUri}/test2.graphql`; const query = 'test'; diff --git a/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts index 6bb5c1062bf..347d517b955 100644 --- a/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts @@ -529,4 +529,36 @@ export function Example(arg: string) {}`; const contents = findGraphQLTags(text, '.svelte'); expect(contents.length).toEqual(1); }); + it('handles full astro example', () => { + const text = ` + --- + const gql = String.raw; + const response = await fetch("https://swapi-graphql.netlify.app/.netlify/functions/index", + { + method: 'POST', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ + query: gql\` + query getFilm ($id:ID!) { + film(id: $id) { + title + releaseDate + } + } + \`, + variables: { + id: "XM6MQ==", + }, + }), + }); + + const json = await response.json(); + const { film } = json.data; + --- +

Fetching information about Star Wars: A New Hope

+

Title: {film.title}

+

Year: {film.releaseDate}

`; + const contents = findGraphQLTags(text, '.astro'); + expect(contents.length).toEqual(1); + }); }); diff --git a/packages/vscode-graphql-syntax/tests/__fixtures__/test.astro b/packages/vscode-graphql-syntax/tests/__fixtures__/test.astro new file mode 100644 index 00000000000..8698868eec3 --- /dev/null +++ b/packages/vscode-graphql-syntax/tests/__fixtures__/test.astro @@ -0,0 +1,30 @@ +--- +const gql = String.raw; +const response = await fetch( + 'https://swapi-graphql.netlify.app/.netlify/functions/index', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: gql` + query getFilm($id: ID!) { + film(id: $id) { + title + releaseDate + } + } + `, + variables: { + id: 'ZmlsbXM6MQ==', + }, + }), + }, +); + +const json = await response.json(); +const { film } = json.data; +--- + +

Fetching information about Star Wars: A New Hope

+

Title: {film.title}

+

Year: {film.releaseDate}

diff --git a/packages/vscode-graphql-syntax/tests/__snapshots__/js-grammar.spec.ts.snap b/packages/vscode-graphql-syntax/tests/__snapshots__/js-grammar.spec.ts.snap index 1be3ebd280f..d9f8179c302 100644 --- a/packages/vscode-graphql-syntax/tests/__snapshots__/js-grammar.spec.ts.snap +++ b/packages/vscode-graphql-syntax/tests/__snapshots__/js-grammar.spec.ts.snap @@ -1,5 +1,69 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`inline.graphql grammar > should tokenize a simple astro file 1`] = ` +--- | +const gql = String.raw; | +const response = await fetch( | + 'https://swapi-graphql.netlify.app/.netlify/functions/index', | + { | + method: 'POST', | + headers: { 'Content-Type': 'application/json' }, | + body: JSON.stringify({ | + query: | + | +gql | entity.name.function.tagged-template.js +\` | punctuation.definition.string.template.begin.js + | meta.embedded.block.graphql +query | meta.embedded.block.graphql keyword.operation.graphql + | meta.embedded.block.graphql +getFilm | meta.embedded.block.graphql entity.name.function.graphql +( | meta.embedded.block.graphql meta.brace.round.graphql +$id | meta.embedded.block.graphql meta.variables.graphql variable.parameter.graphql +: | meta.embedded.block.graphql meta.variables.graphql punctuation.colon.graphql + | meta.embedded.block.graphql meta.variables.graphql +ID | meta.embedded.block.graphql meta.variables.graphql support.type.builtin.graphql +! | meta.embedded.block.graphql meta.variables.graphql keyword.operator.nulltype.graphql +) | meta.embedded.block.graphql meta.brace.round.graphql + | meta.embedded.block.graphql meta.selectionset.graphql +{ | meta.embedded.block.graphql meta.selectionset.graphql punctuation.operation.graphql + | meta.embedded.block.graphql meta.selectionset.graphql +film | meta.embedded.block.graphql meta.selectionset.graphql variable.graphql +( | meta.embedded.block.graphql meta.selectionset.graphql meta.arguments.graphql meta.brace.round.directive.graphql +id | meta.embedded.block.graphql meta.selectionset.graphql meta.arguments.graphql variable.parameter.graphql +: | meta.embedded.block.graphql meta.selectionset.graphql meta.arguments.graphql punctuation.colon.graphql + | meta.embedded.block.graphql meta.selectionset.graphql meta.arguments.graphql +$id | meta.embedded.block.graphql meta.selectionset.graphql meta.arguments.graphql variable.graphql +) | meta.embedded.block.graphql meta.selectionset.graphql meta.arguments.graphql meta.brace.round.directive.graphql + | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql +{ | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql punctuation.operation.graphql + | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql +title | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql variable.graphql + | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql +releaseDate | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql variable.graphql + | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql +} | meta.embedded.block.graphql meta.selectionset.graphql meta.selectionset.graphql punctuation.operation.graphql + | meta.embedded.block.graphql meta.selectionset.graphql +} | meta.embedded.block.graphql meta.selectionset.graphql punctuation.operation.graphql + | meta.embedded.block.graphql +\` | punctuation.definition.string.template.end.js +, | + variables: { | + id: 'ZmlsbXM6MQ==', | + }, | + }), | + }, | +); | + | +const json = await response.json(); | +const { film } = json.data; | +--- | + | +

Fetching information about Star Wars: A New Hope

| +

Title: {film.title}

| +

Year: {film.releaseDate}

| + | +`; + exports[`inline.graphql grammar > should tokenize a simple ecmascript file 1`] = ` /* eslint-disable */ | /* prettier-ignore */ | diff --git a/packages/vscode-graphql-syntax/tests/js-grammar.spec.ts b/packages/vscode-graphql-syntax/tests/js-grammar.spec.ts index 91332e32551..786018e4562 100644 --- a/packages/vscode-graphql-syntax/tests/js-grammar.spec.ts +++ b/packages/vscode-graphql-syntax/tests/js-grammar.spec.ts @@ -23,4 +23,8 @@ describe('inline.graphql grammar', () => { const result = await tokenizeFile('__fixtures__/test.svelte', scope); expect(result).toMatchSnapshot(); }); + it('should tokenize a simple astro file', async () => { + const result = await tokenizeFile('__fixtures__/test.astro', scope); + expect(result).toMatchSnapshot(); + }); }); diff --git a/yarn.lock b/yarn.lock index 96831d6a942..33f78958017 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2387,7 +2387,7 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== @@ -5453,6 +5453,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== +"@types/mock-fs@^4.13.4": + version "4.13.4" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.4.tgz#e73edb4b4889d44d23f1ea02d6eebe50aa30b09a" + integrity sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg== + dependencies: + "@types/node" "*" + "@types/ms@*": version "0.7.31" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" @@ -6050,6 +6057,35 @@ loupe "^2.3.6" pretty-format "^27.5.1" +"@vscode/vsce@^2.19.0", "@vscode/vsce@^2.23.0": + version "2.23.0" + resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-2.23.0.tgz#280ce82356c59efda97d3ba14bcdd9e3e22ddb7f" + integrity sha512-Wf9yN8feZf4XmUW/erXyKQvCL577u72AQv4AI4Cwt5o5NyE49C5mpfw3pN78BJYYG3qnSIxwRo7JPvEurkQuNA== + dependencies: + azure-devops-node-api "^11.0.1" + chalk "^2.4.2" + cheerio "^1.0.0-rc.9" + commander "^6.2.1" + find-yarn-workspace-root "^2.0.0" + glob "^7.0.6" + hosted-git-info "^4.0.2" + jsonc-parser "^3.2.0" + leven "^3.1.0" + markdown-it "^12.3.2" + mime "^1.3.4" + minimatch "^3.0.3" + parse-semver "^1.1.1" + read "^1.0.7" + semver "^7.5.2" + tmp "^0.2.1" + typed-rest-client "^1.8.4" + url-join "^4.0.1" + xml2js "^0.5.0" + yauzl "^2.3.1" + yazl "^2.2.2" + optionalDependencies: + keytar "^7.7.0" + "@vscode/vsce@^2.22.1-2": version "2.22.1-2" resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-2.22.1-2.tgz#0f272f97b23986366ea97e29721cb28410b9af68" @@ -10698,6 +10734,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== +follow-redirects@^1.13.2: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + follow-redirects@^1.14.6: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -15287,15 +15328,28 @@ outdent@^0.5.0: resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== -ovsx@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/ovsx/-/ovsx-0.5.1.tgz#3a8c2707ea120d542d1d226a47f24ac47b078710" - integrity sha512-3OWq0l7DuVHi2bd2aQe5+QVQlFIqvrcw3/2vGXL404L6Tr+R4QHtzfnYYghv8CCa85xJHjU0RhcaC7pyXkAUbg== +ovsx@0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/ovsx/-/ovsx-0.8.3.tgz#3c67a595e423f3f70a3d62da3735dd07dfbca69f" + integrity sha512-LG7wTzy4eYV/KolFeO4AwWPzQSARvCONzd5oHQlNvYOlji2r/zjbdK8pyObZN84uZlk6rQBWrJrAdJfh/SX0Hg== dependencies: + "@vscode/vsce" "^2.19.0" commander "^6.1.0" follow-redirects "^1.14.6" is-ci "^2.0.0" leven "^3.1.0" + semver "^7.5.2" + tmp "^0.2.1" + +ovsx@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ovsx/-/ovsx-0.3.0.tgz#2f30c80c90fbe3c8fc406730c35371219187ca0a" + integrity sha512-UjZjzLt6Iq7LS/XFvEuBAWyn0zmsZEe8fuy5DsbcsIb0mW7PbVtB5Dhe4HeK7NJM228nyhYXC9WCeyBoMi4M3A== + dependencies: + commander "^6.1.0" + follow-redirects "^1.13.2" + is-ci "^2.0.0" + leven "^3.1.0" tmp "^0.2.1" vsce "^2.6.3" From 54658f8dc70e31f3f0b02efdfd662ffeb7c8a948 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 19:19:52 +0100 Subject: [PATCH 02/49] fix schema file cacheing, cache by default --- .../src/MessageProcessor.ts | 89 ++++++++----------- .../src/__tests__/MessageProcessor-test.ts | 7 ++ 2 files changed, 42 insertions(+), 54 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 5c9aea744bc..e136820e5f7 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -256,7 +256,7 @@ export class MessageProcessor { this._handleConfigError({ err }); } } - _handleConfigError({ err }: { err: unknown; uri?: string }) { + private _handleConfigError({ err }: { err: unknown; uri?: string }) { // console.log(err, typeof err); if (err instanceof ConfigNotFoundError || err instanceof ConfigEmptyError) { // TODO: obviously this needs to become a map by workspace from uri @@ -288,14 +288,14 @@ export class MessageProcessor { } } - _logConfigError(errorMessage: string) { + private _logConfigError(errorMessage: string) { this._logger.error( 'WARNING: graphql-config error, only highlighting is enabled:\n' + errorMessage + `\nfor more information on using 'graphql-config' with 'graphql-language-service-server', \nsee the documentation at ${configDocLink}`, ); } - async _isGraphQLConfigFile(uri: string) { + private async _isGraphQLConfigFile(uri: string) { const configMatchers = ['graphql.config', 'graphqlrc', 'graphqlconfig']; if (this._settings?.load?.fileName?.length) { configMatchers.push(this._settings.load.fileName); @@ -539,7 +539,7 @@ export class MessageProcessor { process.exit(this._willShutdown ? 0 : 1); } - validateDocumentAndPosition(params: CompletionParams): void { + private validateDocumentAndPosition(params: CompletionParams): void { if (!params?.textDocument?.uri || !params.position) { throw new Error( '`textDocument.uri` and `position` arguments are required.', @@ -930,11 +930,11 @@ export class MessageProcessor { return []; } - _getTextDocuments() { + private _getTextDocuments() { return Array.from(this._textDocumentCache); } - async _cacheSchemaText(uri: string, text: string, version: number) { + private async _cacheSchemaText(uri: string, text: string, version: number) { try { const contents = this._parser(text, uri); if (contents.length > 0) { @@ -945,11 +945,11 @@ export class MessageProcessor { this._logger.error(String(err)); } } - async _cacheSchemaFile( - _uri: UnnormalizedTypeDefPointer, + private async _cacheSchemaFile( + fileUri: UnnormalizedTypeDefPointer, project: GraphQLProjectConfig, ) { - const uri = _uri.toString(); + const uri = fileUri.toString(); const isFileUri = existsSync(uri); let version = 1; @@ -964,7 +964,7 @@ export class MessageProcessor { await this._cacheSchemaText(schemaUri, schemaText, version); } } - _getTmpProjectPath( + private _getTmpProjectPath( project: GraphQLProjectConfig, prependWithProtocol = true, appendPath?: string, @@ -991,7 +991,7 @@ export class MessageProcessor { * @param uri * @param project */ - async _cacheSchemaPath(uri: string, project: GraphQLProjectConfig) { + private async _cacheSchemaPath(uri: string, project: GraphQLProjectConfig) { try { const files = await glob(uri); if (files && files.length > 0) { @@ -1007,33 +1007,8 @@ export class MessageProcessor { } } catch {} } - async _cacheObjectSchema( - pointer: { [key: string]: any }, - project: GraphQLProjectConfig, - ) { - await Promise.all( - Object.keys(pointer).map(async schemaUri => - this._cacheSchemaPath(schemaUri, project), - ), - ); - } - async _cacheArraySchema( - pointers: UnnormalizedTypeDefPointer[], - project: GraphQLProjectConfig, - ) { - await Promise.all( - pointers.map(async schemaEntry => { - if (typeof schemaEntry === 'string') { - await this._cacheSchemaPath(schemaEntry, project); - } else if (schemaEntry) { - await this._cacheObjectSchema(schemaEntry, project); - } - }), - ); - } - async _cacheSchemaFilesForProject(project: GraphQLProjectConfig) { - const schema = project?.schema; + private async _cacheSchemaFilesForProject(project: GraphQLProjectConfig) { const config = project?.extensions?.languageService; /** * By default, we look for schema definitions in SDL files @@ -1052,15 +1027,21 @@ export class MessageProcessor { const cacheSchemaFileForLookup = config?.cacheSchemaFileForLookup ?? this?._settings?.cacheSchemaFileForLookup ?? - false; - if (cacheSchemaFileForLookup) { + true; + const unwrappedSchema = this._unwrapProjectSchema(project); + const sdlOnly = unwrappedSchema.every( + schemaEntry => + schemaEntry.endsWith('.graphql') || schemaEntry.endsWith('.gql'), + ); + // if we are cacheing the config schema, and it isn't a .graphql file, cache it + if (cacheSchemaFileForLookup && !sdlOnly) { await this._cacheConfigSchema(project); - } else if (typeof schema === 'string') { - await this._cacheSchemaPath(schema, project); - } else if (Array.isArray(schema)) { - await this._cacheArraySchema(schema, project); - } else if (schema) { - await this._cacheObjectSchema(schema, project); + } else if (sdlOnly) { + await Promise.all( + unwrappedSchema.map(async schemaEntry => + this._cacheSchemaFile(schemaEntry, project), + ), + ); } } /** @@ -1068,7 +1049,7 @@ export class MessageProcessor { * from GraphQLCache.getSchema() * @param project {GraphQLProjectConfig} */ - async _cacheConfigSchema(project: GraphQLProjectConfig) { + private async _cacheConfigSchema(project: GraphQLProjectConfig) { try { const schema = await this._graphQLCache.getSchema(project.name); if (schema) { @@ -1115,7 +1096,7 @@ export class MessageProcessor { * * @param project {GraphQLProjectConfig} */ - async _cacheDocumentFilesforProject(project: GraphQLProjectConfig) { + private async _cacheDocumentFilesforProject(project: GraphQLProjectConfig) { try { const documents = await project.getDocuments(); return Promise.all( @@ -1154,7 +1135,7 @@ export class MessageProcessor { * Caching all the document files upfront could be expensive. * @param config {GraphQLConfig} */ - async _cacheAllProjectFiles(config: GraphQLConfig) { + private async _cacheAllProjectFiles(config: GraphQLConfig) { if (config?.projects) { return Promise.all( Object.keys(config.projects).map(async projectName => { @@ -1171,7 +1152,7 @@ export class MessageProcessor { ); } - async _updateFragmentDefinition( + private async _updateFragmentDefinition( uri: Uri, contents: CachedContent[], ): Promise { @@ -1180,7 +1161,7 @@ export class MessageProcessor { await this._graphQLCache.updateFragmentDefinition(rootDir, uri, contents); } - async _updateSchemaIfChanged( + private async _updateSchemaIfChanged( project: GraphQLProjectConfig, uri: Uri, ): Promise { @@ -1195,7 +1176,7 @@ export class MessageProcessor { ); } - _unwrapProjectSchema(project: GraphQLProjectConfig): string[] { + private _unwrapProjectSchema(project: GraphQLProjectConfig): string[] { const projectSchema = project.schema; const schemas: string[] = []; @@ -1216,7 +1197,7 @@ export class MessageProcessor { return schemas; } - async _updateObjectTypeDefinition( + private async _updateObjectTypeDefinition( uri: Uri, contents: CachedContent[], ): Promise { @@ -1225,7 +1206,7 @@ export class MessageProcessor { await this._graphQLCache.updateObjectTypeDefinition(rootDir, uri, contents); } - _getCachedDocument(uri: string): CachedDocumentType | null { + private _getCachedDocument(uri: string): CachedDocumentType | null { if (this._textDocumentCache.has(uri)) { const cachedDocument = this._textDocumentCache.get(uri); if (cachedDocument) { @@ -1235,7 +1216,7 @@ export class MessageProcessor { return null; } - async _invalidateCache( + private async _invalidateCache( textDocument: VersionedTextDocumentIdentifier, uri: Uri, contents: CachedContent[], 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 3f5fe2f4c54..80dcf07b5ee 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -167,6 +167,13 @@ describe('MessageProcessor', () => { ); mockfs.restore(); expect(pkgResult).toEqual(true); + + mockfs({ [`${__dirname}/package.json`]: '{ }' }); + const pkgFalseResult = await messageProcessor._isGraphQLConfigFile( + `file://${__dirname}/package.json`, + ); + mockfs.restore(); + expect(pkgFalseResult).toEqual(false); }); it('runs completion requests properly', async () => { const uri = `${queryPathUri}/test2.graphql`; From 2686b3ff0287ae7c88a0f9e8ca5854639538ac1e Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 19:33:51 +0100 Subject: [PATCH 03/49] more cleanup, glob if present --- .../src/MessageProcessor.ts | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index e136820e5f7..bb606168a75 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -11,7 +11,6 @@ import mkdirp from 'mkdirp'; import { readFileSync, existsSync, writeFileSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; -import glob from 'fast-glob'; import { URI } from 'vscode-uri'; import { CachedContent, @@ -76,6 +75,7 @@ import { SupportedExtensionsEnum, } from './constants'; import { NoopLogger, Logger } from './Logger'; +import glob from 'fast-glob'; const configDocLink = 'https://www.npmjs.com/package/graphql-language-service-server#user-content-graphql-configuration-file'; @@ -984,29 +984,6 @@ export class MessageProcessor { } return path.resolve(projectTmpPath); } - /** - * Safely attempts to cache schema files based on a glob or path - * Exits without warning in several cases because these strings can be almost - * anything! - * @param uri - * @param project - */ - private async _cacheSchemaPath(uri: string, project: GraphQLProjectConfig) { - try { - const files = await glob(uri); - if (files && files.length > 0) { - await Promise.all( - files.map(uriPath => this._cacheSchemaFile(uriPath, project)), - ); - } else { - try { - await this._cacheSchemaFile(uri, project); - } catch { - // this string may be an SDL string even, how do we even evaluate this? - } - } - } catch {} - } private async _cacheSchemaFilesForProject(project: GraphQLProjectConfig) { const config = project?.extensions?.languageService; @@ -1033,7 +1010,7 @@ export class MessageProcessor { schemaEntry => schemaEntry.endsWith('.graphql') || schemaEntry.endsWith('.gql'), ); - // if we are cacheing the config schema, and it isn't a .graphql file, cache it + // if we are caching the config schema, and it isn't a .graphql file, cache it if (cacheSchemaFileForLookup && !sdlOnly) { await this._cacheConfigSchema(project); } else if (sdlOnly) { @@ -1194,7 +1171,19 @@ export class MessageProcessor { schemas.push(...Object.keys(projectSchema)); } - return schemas; + return schemas.reduce((agg, schema) => { + const results = this._globIfFilePattern(schema); + return [...agg, ...results]; + }, []); + } + private _globIfFilePattern(pattern: string) { + if (pattern.includes('*')) { + try { + return glob.sync(pattern); + // URLs may contain * characters + } catch {} + } + return [pattern]; } private async _updateObjectTypeDefinition( From 9d37093f673c6de1f68ffdb0d679cfb0c4d49bd2 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 21:01:04 +0100 Subject: [PATCH 04/49] begin seperating out specs from unit tests --- .../src/MessageProcessor.ts | 2 +- .../src/__tests__/MessageProcessor-test.ts | 624 ------------------ 2 files changed, 1 insertion(+), 625 deletions(-) delete mode 100644 packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index bb606168a75..5843fdaa9c6 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -267,7 +267,7 @@ export class MessageProcessor { // this is the only case where we don't invalidate config; // TODO: per-project schema initialization status (PR is almost ready) this._logConfigError( - 'Project not found for this file - make sure that a schema is present', + 'Project not found for this file - make sure that a schema is present in the config file or for the project', ); } else if (err instanceof ConfigInvalidError) { this._isGraphQLConfigMissing = true; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts deleted file mode 100644 index 80dcf07b5ee..00000000000 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ /dev/null @@ -1,624 +0,0 @@ -/** - * Copyright (c) 2021 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 { SymbolKind } from 'vscode-languageserver'; -import { FileChangeType } from 'vscode-languageserver-protocol'; -import { Position, Range } from 'graphql-language-service'; - -import { MessageProcessor } from '../MessageProcessor'; -import { parseDocument } from '../parseDocument'; - -jest.mock('../Logger'); - -import { GraphQLCache } from '../GraphQLCache'; - -import { loadConfig } from 'graphql-config'; - -import type { DefinitionQueryResult, Outline } from 'graphql-language-service'; - -import { NoopLogger } from '../Logger'; -import { pathToFileURL } from 'node:url'; -import mockfs from 'mock-fs'; -import { join } from 'node:path'; - -jest.mock('node:fs', () => ({ - ...jest.requireActual('fs'), - readFileSync: jest.fn(jest.requireActual('fs').readFileSync), -})); - -describe('MessageProcessor', () => { - const logger = new NoopLogger(); - const messageProcessor = new MessageProcessor({ - // @ts-ignore - connection: {}, - logger, - graphqlFileExtensions: ['graphql'], - loadConfigOptions: { rootDir: __dirname }, - }); - - const queryPathUri = pathToFileURL(`${__dirname}/__queries__`); - const textDocumentTestString = ` - { - hero(episode: NEWHOPE){ - } - } - `; - - beforeEach(async () => { - const gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); - // loadConfig.mockRestore(); - messageProcessor._settings = { load: {} }; - messageProcessor._graphQLCache = new GraphQLCache({ - configDir: __dirname, - config: gqlConfig, - parser: parseDocument, - logger: new NoopLogger(), - }); - messageProcessor._languageService = { - // @ts-ignore - getAutocompleteSuggestions(query, position, uri) { - return [{ label: `${query} at ${uri}` }]; - }, - // @ts-ignore - getDiagnostics(_query, _uri) { - return []; - }, - async getDocumentSymbols(_query: string, uri: string) { - return [ - { - name: 'item', - kind: SymbolKind.Field, - location: { - uri, - range: { - start: { line: 1, character: 2 }, - end: { line: 1, character: 4 }, - }, - }, - }, - ]; - }, - async getOutline(_query: string): Promise { - return { - outlineTrees: [ - { - representativeName: 'item', - kind: 'Field', - startPosition: new Position(1, 2), - endPosition: new Position(1, 4), - children: [], - }, - ], - }; - }, - async getDefinition( - _query, - position, - uri, - ): Promise { - return { - queryRange: [new Range(position, position)], - definitions: [ - { - position, - path: uri, - }, - ], - }; - }, - }; - }); - - let getConfigurationReturnValue = {}; - // @ts-ignore - messageProcessor._connection = { - // @ts-ignore - get workspace() { - return { - async getConfiguration() { - return [getConfigurationReturnValue]; - }, - }; - }, - }; - - const initialDocument = { - textDocument: { - text: textDocumentTestString, - uri: `${queryPathUri}/test.graphql`, - version: 0, - }, - }; - - messageProcessor._isInitialized = true; - - it('initializes properly and opens a file', async () => { - const { capabilities } = await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: __dirname, - }, - null, - __dirname, - ); - expect(capabilities.definitionProvider).toEqual(true); - expect(capabilities.workspaceSymbolProvider).toEqual(true); - expect(capabilities.completionProvider.resolveProvider).toEqual(true); - expect(capabilities.textDocumentSync).toEqual(1); - }); - it('detects a config file', async () => { - const result = await messageProcessor._isGraphQLConfigFile( - 'graphql.config.js', - ); - expect(result).toEqual(true); - const falseResult = await messageProcessor._isGraphQLConfigFile( - 'graphql.js', - ); - expect(falseResult).toEqual(false); - - mockfs({ [`${__dirname}/package.json`]: '{"graphql": {}}' }); - const pkgResult = await messageProcessor._isGraphQLConfigFile( - `file://${__dirname}/package.json`, - ); - mockfs.restore(); - expect(pkgResult).toEqual(true); - - mockfs({ [`${__dirname}/package.json`]: '{ }' }); - const pkgFalseResult = await messageProcessor._isGraphQLConfigFile( - `file://${__dirname}/package.json`, - ); - mockfs.restore(); - expect(pkgFalseResult).toEqual(false); - }); - it('runs completion requests properly', async () => { - const uri = `${queryPathUri}/test2.graphql`; - const query = 'test'; - messageProcessor._textDocumentCache.set(uri, { - version: 0, - contents: [ - { - query, - range: new Range(new Position(0, 0), new Position(0, 0)), - }, - ], - }); - - const test = { - position: new Position(0, 0), - textDocument: { uri }, - }; - const result = await messageProcessor.handleCompletionRequest(test); - expect(result).toEqual({ - items: [{ label: `${query} at ${uri}` }], - isIncomplete: false, - }); - }); - - it('runs document symbol requests', async () => { - const uri = `${queryPathUri}/test3.graphql`; - const validQuery = ` - { - hero(episode: EMPIRE){ - ...testFragment - } - } - `; - - const newDocument = { - textDocument: { - text: validQuery, - uri, - version: 0, - }, - }; - - messageProcessor._textDocumentCache.set(uri, { - version: 0, - contents: [ - { - query: validQuery, - range: new Range(new Position(0, 0), new Position(0, 0)), - }, - ], - }); - - const test = { - textDocument: newDocument.textDocument, - }; - - const result = await messageProcessor.handleDocumentSymbolRequest(test); - - expect(result).not.toBeUndefined(); - expect(result.length).toEqual(1); - expect(result[0].name).toEqual('item'); - expect(result[0].kind).toEqual(SymbolKind.Field); - expect(result[0].location.range).toEqual({ - start: { line: 1, character: 2 }, - end: { line: 1, character: 4 }, - }); - }); - - it('properly changes the file cache with the didChange handler', async () => { - const uri = `${queryPathUri}/test.graphql`; - messageProcessor._textDocumentCache.set(uri, { - version: 1, - contents: [ - { - query: '', - range: new Range(new Position(0, 0), new Position(0, 0)), - }, - ], - }); - const textDocumentChangedString = ` - { - hero(episode: NEWHOPE){ - name - } - } - `; - - const result = await messageProcessor.handleDidChangeNotification({ - textDocument: { - // @ts-ignore - text: textDocumentTestString, - uri, - version: 1, - }, - contentChanges: [ - { text: textDocumentTestString }, - { text: textDocumentChangedString }, - ], - }); - // Query fixed, no more errors - expect(result.diagnostics.length).toEqual(0); - }); - - it('does not crash on null value returned in response to workspace configuration', async () => { - const previousConfigurationValue = getConfigurationReturnValue; - getConfigurationReturnValue = null; - await expect( - messageProcessor.handleDidChangeConfiguration(), - ).resolves.toStrictEqual({}); - getConfigurationReturnValue = previousConfigurationValue; - }); - - it('properly removes from the file cache with the didClose handler', async () => { - await messageProcessor.handleDidCloseNotification(initialDocument); - - const position = { line: 4, character: 5 }; - const params = { textDocument: initialDocument.textDocument, position }; - - // Should throw because file has been deleted from cache - try { - const result = await messageProcessor.handleCompletionRequest(params); - expect(result).toEqual(null); - } catch {} - }); - - // modified to work with jest.mock() of WatchmanClient - it('runs definition requests', async () => { - jest.setTimeout(10000); - const validQuery = ` - { - hero(episode: EMPIRE){ - ...testFragment - } - } - `; - - const newDocument = { - textDocument: { - text: validQuery, - uri: `${queryPathUri}/test3.graphql`, - 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); - - const test = { - position: new Position(3, 15), - textDocument: newDocument.textDocument, - }; - - const result = await messageProcessor.handleDefinitionRequest(test); - await expect(result[0].uri).toEqual(`${queryPathUri}/test3.graphql`); - }); - - describe('handleDidOpenOrSaveNotification', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._updateGraphQLConfig = jest.fn(); - }); - it('updates config for standard config filename changes', async () => { - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/.graphql.config.js`, - languageId: 'js', - version: 0, - text: '', - }, - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - }); - - it('updates config for custom config filename changes', async () => { - const customConfigName = 'custom-config-name.yml'; - messageProcessor._settings = { load: { fileName: customConfigName } }; - - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/${customConfigName}`, - languageId: 'js', - version: 0, - text: '', - }, - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - }); - - it('handles config requests with no config', async () => { - messageProcessor._settings = {}; - - await messageProcessor.handleDidChangeConfiguration({ - settings: [], - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/.graphql.config.js`, - languageId: 'js', - version: 0, - text: '', - }, - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - }); - }); - - describe('handleWatchedFilesChangedNotification', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._updateGraphQLConfig = jest.fn(); - }); - - it('skips config updates for normal file changes', async () => { - await messageProcessor.handleWatchedFilesChangedNotification({ - changes: [ - { - uri: `${pathToFileURL('.')}/foo.graphql`, - type: FileChangeType.Changed, - }, - ], - }); - - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); - }); - }); - - describe('handleWatchedFilesChangedNotification without graphql config', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._graphQLConfig = undefined; - messageProcessor._isGraphQLConfigMissing = true; - messageProcessor._parser = jest.fn(); - }); - - it('skips config updates for normal file changes', async () => { - await messageProcessor.handleWatchedFilesChangedNotification({ - changes: [ - { - uri: `${pathToFileURL('.')}/foo.js`, - type: FileChangeType.Changed, - }, - ], - }); - expect(messageProcessor._parser).not.toHaveBeenCalled(); - }); - }); - - describe('handleDidChangedNotification without graphql config', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._graphQLConfig = undefined; - messageProcessor._isGraphQLConfigMissing = true; - messageProcessor._parser = jest.fn(); - }); - - it('skips config updates for normal file changes', async () => { - await messageProcessor.handleDidChangeNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/foo.js`, - version: 1, - }, - contentChanges: [{ text: 'var something' }], - }); - expect(messageProcessor._parser).not.toHaveBeenCalled(); - }); - }); -}); - -describe('MessageProcessor with no config', () => { - let messageProcessor: MessageProcessor; - const mockRoot = join('/tmp', 'test'); - let loggerSpy: jest.SpyInstance; - - const mockProcessor = (query: string, config?: string) => { - const items = { - 'query.graphql': query, - 'node_modules/parse-json': mockfs.load('node_modules/parse-json'), - }; - if (config) { - items['graphql.config.js'] = config; - } - const files: Record> = { - [mockRoot]: mockfs.directory({ - items, - }), - // node_modules: mockfs.load('node_modules'), - }; - mockfs(files); - const logger = new NoopLogger(); - loggerSpy = jest.spyOn(logger, 'error'); - messageProcessor = new MessageProcessor({ - // @ts-ignore - connection: { - // @ts-ignore - get workspace() { - return { - async getConfiguration() { - return []; - }, - }; - }, - }, - logger, - graphqlFileExtensions: ['graphql'], - loadConfigOptions: { rootDir: mockRoot }, - }); - }; - - beforeEach(() => {}); - - afterEach(() => { - mockfs.restore(); - }); - it('fails to initialize with empty config file', async () => { - mockProcessor('query { foo }', ''); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringMatching( - /GraphQL Config file is not available in the provided config directory/, - ), - ); - }); - it('fails to initialize with no config file present', async () => { - mockProcessor('query { foo }'); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringMatching( - /GraphQL Config file is not available in the provided config directory/, - ), - ); - }); - it('initializes when presented with a valid config later', async () => { - mockProcessor('query { foo }'); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(loggerSpy).toHaveBeenCalledTimes(1); - // todo: get mockfs working with in-test file changes - // mockfs.restore(); - // mockfs({ - // [mockRoot]: mockfs.directory({ - // mode: 0o755, - // items: { - // 'schema.graphql': - // 'type Query { foo: String }\nschema { query: Query }', - // 'graphql.config.js': mockfs.file({ - // content: 'module.exports = { schema: "schema.graphql" };', - // mode: 0o644, - // }), - // 'query.graphql': 'query { foo }', - // // 'node_modules/graphql-config/node_modules': mockfs.load( - // // 'node_modules/graphql-config/node_modules', - // // ), - // }, - // }), - // }); - // // console.log(readdirSync(`${mockRoot}`)); - // await messageProcessor.handleDidOpenOrSaveNotification({ - // textDocument: { - // text: 'module.exports = { schema: `schema.graphql` }', - // uri: `${mockRoot}/graphql.config.js`, - // version: 2, - // }, - // }); - - // expect(messageProcessor._isGraphQLConfigMissing).toEqual(false); - - // expect(loggerSpy).toHaveBeenCalledWith( - // expect.stringMatching( - // /GraphQL Config file is not available in the provided config directory/, - // ), - // ); - }); -}); From 283561e63f33d2a61abf8ddeef96feac2feccc56 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 22:12:07 +0100 Subject: [PATCH 05/49] more unit coverage --- .../graphql-language-service-server/src/MessageProcessor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 5843fdaa9c6..b09e09f9450 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -912,6 +912,7 @@ export class MessageProcessor { await Promise.all( documents.map(async ([uri]) => { const cachedDocument = this._getCachedDocument(uri); + if (!cachedDocument) { return []; } @@ -1233,7 +1234,7 @@ export class MessageProcessor { } } -function processDiagnosticsMessage( +export function processDiagnosticsMessage( results: Diagnostic[], query: string, range: RangeType | null, From 8737a893782433fe0102fcf3988dc9ba46b6cd99 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 22:12:26 +0100 Subject: [PATCH 06/49] more unit coverage --- .../src/__tests__/MessageProcessor.spec.ts | 166 +++++ .../src/__tests__/MessageProcessor.test.ts | 678 ++++++++++++++++++ 2 files changed, 844 insertions(+) create mode 100644 packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts create mode 100644 packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts new file mode 100644 index 00000000000..3a61509fff8 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -0,0 +1,166 @@ +import { MessageProcessor } from '../MessageProcessor'; + +jest.mock('../Logger'); + +import { NoopLogger } from '../Logger'; +import mockfs from 'mock-fs'; +import { join } from 'node:path'; + +describe('MessageProcessor with no config', () => { + let messageProcessor: MessageProcessor; + const mockRoot = join('/tmp', 'test'); + let loggerSpy: jest.SpyInstance; + let mockProcessor; + + beforeEach(() => { + mockProcessor = (query: string, config?: string) => { + const items = { + 'query.graphql': query, + }; + if (config) { + items['graphql.config.js'] = config; + } + const files: Parameters[0] = { + [mockRoot]: mockfs.directory({ + items, + }), + 'node_modules/parse-json': mockfs.load('node_modules/parse-json'), + 'node_modules/error-ex': mockfs.load('node_modules/error-ex'), + 'node_modules/is-arrayish': mockfs.load('node_modules/is-arrayish'), + 'node_modules/json-parse-even-better-errors': mockfs.load( + 'node_modules/json-parse-even-better-errors', + ), + 'node_modules/lines-and-columns': mockfs.load( + 'node_modules/lines-and-columns', + ), + 'node_modules/@babel': mockfs.load('node_modules/@babel'), + }; + mockfs(files); + const logger = new NoopLogger(); + loggerSpy = jest.spyOn(logger, 'error'); + messageProcessor = new MessageProcessor({ + // @ts-ignore + connection: { + // @ts-ignore + get workspace() { + return { + async getConfiguration() { + return []; + }, + }; + }, + }, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: mockRoot }, + }); + }; + }); + + afterEach(() => { + mockfs.restore(); + }); + it('fails to initialize with empty config file', async () => { + mockProcessor('query { foo }', ''); + await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: mockRoot, + }, + null, + mockRoot, + ); + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + text: 'query { foo }', + uri: `${mockRoot}/query.graphql`, + version: 1, + }, + }); + expect(messageProcessor._isInitialized).toEqual(false); + expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringMatching( + /GraphQL Config file is not available in the provided config directory/, + ), + ); + }); + it('fails to initialize with no config file present', async () => { + mockProcessor('query { foo }'); + await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: mockRoot, + }, + null, + mockRoot, + ); + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + text: 'query { foo }', + uri: `${mockRoot}/query.graphql`, + version: 1, + }, + }); + expect(messageProcessor._isInitialized).toEqual(false); + expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringMatching( + /GraphQL Config file is not available in the provided config directory/, + ), + ); + }); + it('initializes when presented with a valid config later', async () => { + mockProcessor('query { foo }'); + await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: mockRoot, + }, + null, + mockRoot, + ); + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + text: 'query { foo }', + uri: `${mockRoot}/query.graphql`, + version: 1, + }, + }); + expect(messageProcessor._isInitialized).toEqual(false); + expect(loggerSpy).toHaveBeenCalledTimes(1); + // todo: get mockfs working with in-test file changes + // mockfs({ + // [mockRoot]: mockfs.directory({ + // mode: 0o755, + // items: { + // 'schema.graphql': + // 'type Query { foo: String }\nschema { query: Query }', + // 'graphql.config.js': mockfs.file({ + // content: 'module.exports = { schema: "schema.graphql" };', + // mode: 0o644, + // }), + // 'query.graphql': 'query { foo }', + // }, + // }), + // }); + // // console.log(readdirSync(`${mockRoot}`)); + // await messageProcessor.handleDidOpenOrSaveNotification({ + // textDocument: { + // text: 'module.exports = { schema: "schema.graphql" }', + // uri: `${mockRoot}/graphql.config.js`, + // version: 2, + // }, + // }); + + // expect(messageProcessor._isGraphQLConfigMissing).toEqual(false); + + // expect(loggerSpy).toHaveBeenCalledWith( + // expect.stringMatching( + // /GraphQL Config file is not available in the provided config directory/, + // ), + // ); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts new file mode 100644 index 00000000000..2c2ddf3768b --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -0,0 +1,678 @@ +/** + * Copyright (c) 2021 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 { SymbolKind } from 'vscode-languageserver'; +import { FileChangeType } from 'vscode-languageserver-protocol'; +import { Position, Range } from 'graphql-language-service'; + +import { + MessageProcessor, + processDiagnosticsMessage, +} from '../MessageProcessor'; +import { parseDocument } from '../parseDocument'; + +jest.mock('../Logger'); + +import { GraphQLCache, getGraphQLCache } from '../GraphQLCache'; + +import { loadConfig } from 'graphql-config'; + +import type { DefinitionQueryResult, Outline } from 'graphql-language-service'; + +import { NoopLogger } from '../Logger'; +import { pathToFileURL } from 'node:url'; +import mockfs from 'mock-fs'; + +jest.mock('node:fs', () => ({ + ...jest.requireActual('fs'), + readFileSync: jest.fn(jest.requireActual('fs').readFileSync), +})); + +describe('MessageProcessor', () => { + const logger = new NoopLogger(); + const messageProcessor = new MessageProcessor({ + // @ts-ignore + connection: {}, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + }); + + const queryPathUri = pathToFileURL(`${__dirname}/__queries__`); + const textDocumentTestString = ` + { + hero(episode: NEWHOPE){ + } + } + `; + + beforeEach(async () => { + const gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); + // loadConfig.mockRestore(); + messageProcessor._settings = { load: {} }; + messageProcessor._graphQLCache = new GraphQLCache({ + configDir: __dirname, + config: gqlConfig, + parser: parseDocument, + logger: new NoopLogger(), + }); + messageProcessor._languageService = { + // @ts-ignore + getAutocompleteSuggestions(query, position, uri) { + return [{ label: `${query} at ${uri}` }]; + }, + // @ts-ignore + getDiagnostics(_query, _uri) { + return []; + }, + async getHoverInformation(_query, position, uri) { + return { + contents: '```graphql\nField: hero\n```', + range: new Range(position, position), + }; + }, + async getDocumentSymbols(_query: string, uri: string) { + return [ + { + name: 'item', + kind: SymbolKind.Field, + location: { + uri, + range: { + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }, + }, + }, + ]; + }, + async getOutline(_query: string): Promise { + return { + outlineTrees: [ + { + representativeName: 'item', + kind: 'Field', + startPosition: new Position(1, 2), + endPosition: new Position(1, 4), + children: [], + }, + ], + }; + }, + async getDefinition( + _query, + position, + uri, + ): Promise { + return { + queryRange: [new Range(position, position)], + definitions: [ + { + position, + path: uri, + }, + ], + }; + }, + }; + }); + + let getConfigurationReturnValue = {}; + // @ts-ignore + messageProcessor._connection = { + // @ts-ignore + get workspace() { + return { + async getConfiguration() { + return [getConfigurationReturnValue]; + }, + }; + }, + }; + + const initialDocument = { + textDocument: { + text: textDocumentTestString, + uri: `${queryPathUri}/test.graphql`, + version: 0, + }, + }; + + messageProcessor._isInitialized = true; + + it('initializes properly and opens a file', async () => { + const { capabilities } = await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + expect(capabilities.definitionProvider).toEqual(true); + expect(capabilities.workspaceSymbolProvider).toEqual(true); + expect(capabilities.completionProvider.resolveProvider).toEqual(true); + expect(capabilities.textDocumentSync).toEqual(1); + }); + it('detects a config file', async () => { + const result = await messageProcessor._isGraphQLConfigFile( + 'graphql.config.js', + ); + expect(result).toEqual(true); + const falseResult = await messageProcessor._isGraphQLConfigFile( + 'graphql.js', + ); + expect(falseResult).toEqual(false); + + mockfs({ [`${__dirname}/package.json`]: '{"graphql": {}}' }); + const pkgResult = await messageProcessor._isGraphQLConfigFile( + `file://${__dirname}/package.json`, + ); + mockfs.restore(); + expect(pkgResult).toEqual(true); + + mockfs({ [`${__dirname}/package.json`]: '{ }' }); + const pkgFalseResult = await messageProcessor._isGraphQLConfigFile( + `file://${__dirname}/package.json`, + ); + mockfs.restore(); + expect(pkgFalseResult).toEqual(false); + }); + it('runs completion requests properly', async () => { + const uri = `${queryPathUri}/test2.graphql`; + const query = 'test'; + messageProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query, + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + + const test = { + position: new Position(0, 0), + textDocument: { uri }, + }; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual({ + items: [{ label: `${query} at ${uri}` }], + isIncomplete: false, + }); + }); + + it('runs document symbol requests', async () => { + const uri = `${queryPathUri}/test3.graphql`; + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri, + version: 0, + }, + }; + + messageProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + + const test = { + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDocumentSymbolRequest(test); + + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('item'); + expect(result[0].kind).toEqual(SymbolKind.Field); + expect(result[0].location.range).toEqual({ + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }); + }); + + it('properly changes the file cache with the didChange handler', async () => { + const uri = `${queryPathUri}/test.graphql`; + messageProcessor._textDocumentCache.set(uri, { + version: 1, + contents: [ + { + query: '', + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + const textDocumentChangedString = ` + { + hero(episode: NEWHOPE){ + name + } + } + `; + + const result = await messageProcessor.handleDidChangeNotification({ + textDocument: { + // @ts-ignore + text: textDocumentTestString, + uri, + version: 1, + }, + contentChanges: [ + { text: textDocumentTestString }, + { text: textDocumentChangedString }, + ], + }); + // Query fixed, no more errors + expect(result.diagnostics.length).toEqual(0); + }); + + it('does not crash on null value returned in response to workspace configuration', async () => { + const previousConfigurationValue = getConfigurationReturnValue; + getConfigurationReturnValue = null; + await expect( + messageProcessor.handleDidChangeConfiguration(), + ).resolves.toStrictEqual({}); + getConfigurationReturnValue = previousConfigurationValue; + }); + + it('properly removes from the file cache with the didClose handler', async () => { + await messageProcessor.handleDidCloseNotification(initialDocument); + + const position = { line: 4, character: 5 }; + const params = { textDocument: initialDocument.textDocument, position }; + + // Should throw because file has been deleted from cache + try { + const result = await messageProcessor.handleCompletionRequest(params); + expect(result).toEqual(null); + } catch {} + }); + + // modified to work with jest.mock() of WatchmanClient + it('runs definition requests', async () => { + jest.setTimeout(10000); + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryPathUri}/test3.graphql`, + 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); + + const test = { + position: new Position(3, 15), + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDefinitionRequest(test); + await expect(result[0].uri).toEqual(`${queryPathUri}/test3.graphql`); + }); + it('runs hover requests', async () => { + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryPathUri}/test4.graphql`, + 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); + + const test = { + position: new Position(3, 15), + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleHoverRequest(test); + expect(JSON.stringify(result.contents)).toEqual( + JSON.stringify({ + contents: '```graphql\nField: hero\n```', + range: new Range(new Position(3, 15), new Position(3, 15)), + }), + ); + }); + it('runs hover request with no file present', async () => { + const test = { + position: new Position(3, 15), + textDocument: { + uri: `${queryPathUri}/test5.graphql`, + version: 1, + }, + }; + messageProcessor._getCachedDocument = (_uri: string) => null; + + const result = await messageProcessor.handleHoverRequest(test); + expect(result).toEqual({ contents: [] }); + }); + + it('runs workspace symbol requests', async () => { + const msgProcessor = new MessageProcessor({ + // @ts-ignore + connection: {}, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + }); + await msgProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + const uri = `${queryPathUri}/test6.graphql`; + const docUri = `${queryPathUri}/test7.graphql`; + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + const validDocument = ` + fragment testFragment on Character { + name + }`; + msgProcessor._graphQLCache = new GraphQLCache({ + configDir: __dirname, + config: await loadConfig({ rootDir: __dirname }), + parser: parseDocument, + logger: new NoopLogger(), + }); + msgProcessor._languageService = { + getDocumentSymbols: async () => [ + { + name: 'testFragment', + kind: SymbolKind.Field, + location: { + uri, + range: { + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }, + }, + }, + ], + }; + msgProcessor._isInitialized = true; + msgProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(6, 0)), + }, + ], + }); + + await msgProcessor._graphQLCache.updateFragmentDefinition( + __dirname, + docUri, + [ + { + query: validDocument, + range: new Range(new Position(0, 0), new Position(4, 0)), + }, + ], + ); + + const test = { + query: 'testFragment', + }; + + const result = await msgProcessor.handleWorkspaceSymbolRequest(test); + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('testFragment'); + expect(result[0].kind).toEqual(SymbolKind.Field); + expect(result[0].location.range).toEqual({ + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }); + }); + + describe('handleDidOpenOrSaveNotification', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._updateGraphQLConfig = jest.fn(); + }); + it('updates config for standard config filename changes', async () => { + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/.graphql.config.js`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + }); + + it('updates config for custom config filename changes', async () => { + const customConfigName = 'custom-config-name.yml'; + messageProcessor._settings = { load: { fileName: customConfigName } }; + + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/${customConfigName}`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + }); + + it('handles config requests with no config', async () => { + messageProcessor._settings = {}; + + await messageProcessor.handleDidChangeConfiguration({ + settings: [], + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/.graphql.config.js`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + }); + }); + + describe('handleWatchedFilesChangedNotification', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._updateGraphQLConfig = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: `${pathToFileURL('.')}/foo.graphql`, + type: FileChangeType.Changed, + }, + ], + }); + + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + }); + }); + + describe('handleWatchedFilesChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._graphQLConfig = undefined; + messageProcessor._isGraphQLConfigMissing = true; + messageProcessor._parser = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: `${pathToFileURL('.')}/foo.js`, + type: FileChangeType.Changed, + }, + ], + }); + expect(messageProcessor._parser).not.toHaveBeenCalled(); + }); + }); + + describe('handleDidChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._graphQLConfig = undefined; + messageProcessor._isGraphQLConfigMissing = true; + messageProcessor._parser = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleDidChangeNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/foo.js`, + version: 1, + }, + contentChanges: [{ text: 'var something' }], + }); + expect(messageProcessor._parser).not.toHaveBeenCalled(); + }); + }); +}); + +describe('processDiagnosticsMessage', () => { + it('processes diagnostics messages', () => { + const query = 'query { foo }'; + const inputRange = new Range(new Position(1, 1), new Position(1, 1)); + + const diagnostics = processDiagnosticsMessage( + [ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: inputRange, + }, + ], + query, + inputRange, + ); + + expect(JSON.stringify(diagnostics)).toEqual( + JSON.stringify([ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: new Range(new Position(2, 1), new Position(2, 1)), + }, + ]), + ); + }); + it('processes diagnostics messages with null range', () => { + const query = 'query { foo }'; + const inputRange = new Range(new Position(1, 1), new Position(1, 1)); + + const diagnostics = processDiagnosticsMessage( + [ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: inputRange, + }, + ], + query, + null, + ); + + expect(JSON.stringify(diagnostics)).toEqual( + JSON.stringify([ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: inputRange, + }, + ]), + ); + }); +}); From d83a7bf5f204ec651ec24a6e3dcc8d53f0f7ceda Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 27 Jan 2024 22:17:59 +0100 Subject: [PATCH 07/49] spelling error --- custom-words.txt | 1 + .../src/__tests__/MessageProcessor.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/custom-words.txt b/custom-words.txt index 297692fd04b..953fcd65eda 100644 --- a/custom-words.txt +++ b/custom-words.txt @@ -65,6 +65,7 @@ yoshiakis // packages and tools argparse +arrayish astro astrojs changesets 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 2c2ddf3768b..143e2bb751f 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -18,7 +18,7 @@ import { parseDocument } from '../parseDocument'; jest.mock('../Logger'); -import { GraphQLCache, getGraphQLCache } from '../GraphQLCache'; +import { GraphQLCache } from '../GraphQLCache'; import { loadConfig } from 'graphql-config'; @@ -70,7 +70,7 @@ describe('MessageProcessor', () => { getDiagnostics(_query, _uri) { return []; }, - async getHoverInformation(_query, position, uri) { + async getHoverInformation(_query, position, _uri) { return { contents: '```graphql\nField: hero\n```', range: new Range(position, position), From a9d0491536948ed15cb9e797e46515b2113f63a1 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 28 Jan 2024 11:54:30 +0100 Subject: [PATCH 08/49] test empty cases --- .../src/MessageProcessor.ts | 17 ++++---- .../src/__tests__/MessageProcessor.test.ts | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index b09e09f9450..d8bfe10a834 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -194,9 +194,6 @@ export class MessageProcessor { 'no rootPath configured in extension or server, defaulting to cwd', ); } - if (!serverCapabilities) { - throw new Error('GraphQL Language Server is not initialized.'); - } this._logger.info( JSON.stringify({ @@ -327,7 +324,7 @@ export class MessageProcessor { params.textDocument.uri, ); try { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { // don't try to initialize again if we've already tried // and the graphql config file or package.json entry isn't even there if (this._isGraphQLConfigMissing === true && !isGraphQLConfigFile) { @@ -504,7 +501,7 @@ export class MessageProcessor { } handleDidCloseNotification(params: DidCloseTextDocumentParams): void { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return; } // For every `textDocument/didClose` event, delete the cached entry. @@ -550,7 +547,7 @@ export class MessageProcessor { async handleCompletionRequest( params: CompletionParams, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } @@ -606,7 +603,7 @@ export class MessageProcessor { } async handleHoverRequest(params: TextDocumentPositionParams): Promise { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return { contents: [] }; } @@ -743,7 +740,7 @@ export class MessageProcessor { params: TextDocumentPositionParams, _token?: CancellationToken, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } @@ -840,7 +837,7 @@ export class MessageProcessor { async handleDocumentSymbolRequest( params: DocumentSymbolParams, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } @@ -900,7 +897,7 @@ export class MessageProcessor { async handleWorkspaceSymbolRequest( params: WorkspaceSymbolParams, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } // const config = await this._graphQLCache.getGraphQLConfig(); 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 143e2bb751f..b0dafc3051e 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -206,8 +206,26 @@ describe('MessageProcessor', () => { isIncomplete: false, }); }); + it('runs completion requests properly with no file present', async () => { + const test = { + position: new Position(0, 0), + textDocument: { uri: `${queryPathUri}/test13.graphql` }, + }; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual([]); + }); + it('runs completion requests properly when not initialized', async () => { + const test = { + position: new Position(0, 3), + textDocument: { uri: `${queryPathUri}/test2.graphql` }, + }; + messageProcessor._isInitialized = false; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual([]); + }); it('runs document symbol requests', async () => { + messageProcessor._isInitialized = true; const uri = `${queryPathUri}/test3.graphql`; const validQuery = ` { @@ -250,6 +268,29 @@ describe('MessageProcessor', () => { end: { line: 1, character: 4 }, }); }); + it('runs document symbol requests with no file present', async () => { + const test = { + textDocument: { + uri: `${queryPathUri}/test4.graphql`, + version: 0, + }, + }; + + const result = await messageProcessor.handleDocumentSymbolRequest(test); + expect(result).toEqual([]); + }); + it('runs document symbol requests when not initialized', async () => { + const test = { + textDocument: { + uri: `${queryPathUri}/test5.graphql`, + version: 0, + }, + }; + messageProcessor._isInitialized = false; + const result = await messageProcessor.handleDocumentSymbolRequest(test); + expect(result).toEqual([]); + messageProcessor._isInitialized = true; + }); it('properly changes the file cache with the didChange handler', async () => { const uri = `${queryPathUri}/test.graphql`; From d4ede8ef1fab8029c4f72be29244e11a54683297 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Thu, 1 Feb 2024 23:15:19 +0100 Subject: [PATCH 09/49] config error handling --- .../src/__tests__/MessageProcessor.spec.ts | 1 + .../src/__tests__/MessageProcessor.test.ts | 75 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 3a61509fff8..b2f7a14546f 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -131,6 +131,7 @@ describe('MessageProcessor with no config', () => { }); expect(messageProcessor._isInitialized).toEqual(false); expect(loggerSpy).toHaveBeenCalledTimes(1); + // todo: get mockfs working with in-test file changes // mockfs({ // [mockRoot]: mockfs.directory({ 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 b0dafc3051e..4f9a18dca9d 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -20,13 +20,20 @@ jest.mock('../Logger'); import { GraphQLCache } from '../GraphQLCache'; -import { loadConfig } from 'graphql-config'; +import { + ConfigInvalidError, + ConfigNotFoundError, + LoaderNoResultError, + ProjectNotFoundError, + loadConfig, +} from 'graphql-config'; import type { DefinitionQueryResult, Outline } from 'graphql-language-service'; import { NoopLogger } from '../Logger'; import { pathToFileURL } from 'node:url'; import mockfs from 'mock-fs'; +import { join } from 'node:path'; jest.mock('node:fs', () => ({ ...jest.requireActual('fs'), @@ -587,27 +594,89 @@ describe('MessageProcessor', () => { expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); }); }); + describe('_handleConfigErrors', () => { + it('handles missing config errors', async () => { + messageProcessor._handleConfigError({ + err: new ConfigNotFoundError('test missing-config'), + uri: 'test', + }); + + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('test missing-config'), + ); + }); + it('handles missing project errors', async () => { + messageProcessor._handleConfigError({ + err: new ProjectNotFoundError('test missing-project'), + uri: 'test', + }); + + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Project not found for this file'), + ); + }); + it('handles invalid config errors', async () => { + messageProcessor._handleConfigError({ + err: new ConfigInvalidError('test invalid error'), + uri: 'test', + }); + + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid configuration'), + ); + }); + it('handles empty loader result errors', async () => { + messageProcessor._handleConfigError({ + err: new LoaderNoResultError('test loader-error'), + uri: 'test', + }); + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('test loader-error'), + ); + }); + it('handles generic errors', async () => { + messageProcessor._handleConfigError({ + err: new Error('test loader-error'), + uri: 'test', + }); + + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('test loader-error'), + ); + }); + }); describe('handleWatchedFilesChangedNotification', () => { const mockReadFileSync: jest.Mock = jest.requireMock('node:fs').readFileSync; beforeEach(() => { - mockReadFileSync.mockReturnValue(''); + mockReadFileSync.mockReturnValue(' query { id }'); messageProcessor._updateGraphQLConfig = jest.fn(); + messageProcessor._updateFragmentDefinition = jest.fn(); + messageProcessor._isGraphQLConfigMissing = false; + messageProcessor._isInitialized = true; }); it('skips config updates for normal file changes', async () => { await messageProcessor.handleWatchedFilesChangedNotification({ changes: [ { - uri: `${pathToFileURL('.')}/foo.graphql`, + uri: `${pathToFileURL( + join(__dirname, '__queries__'), + )}/test.graphql`, type: FileChangeType.Changed, }, ], }); expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._updateFragmentDefinition).toHaveBeenCalled(); }); }); From 7da3b6d6c52ba65215b808b79b84182fd1b6571a Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 4 Feb 2024 13:40:14 +0100 Subject: [PATCH 10/49] add integration spec coverage for cacheing the schema file! --- .../src/MessageProcessor.ts | 9 ++- .../src/__tests__/MessageProcessor.spec.ts | 68 ++++++++++++++++ .../src/__tests__/__utils__/MockProject.ts | 78 +++++++++++++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index d8bfe10a834..3bca4f1123e 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -7,8 +7,7 @@ * */ -import mkdirp from 'mkdirp'; -import { readFileSync, existsSync, writeFileSync } from 'node:fs'; +import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; import { URI } from 'vscode-uri'; @@ -147,7 +146,7 @@ export class MessageProcessor { } if (!existsSync(this._tmpDirBase)) { - void mkdirp(this._tmpDirBase); + mkdirSync(this._tmpDirBase); } } get connection(): Connection { @@ -972,7 +971,9 @@ export class MessageProcessor { const basePath = path.join(this._tmpDirBase, workspaceName); let projectTmpPath = path.join(basePath, 'projects', project.name); if (!existsSync(projectTmpPath)) { - void mkdirp(projectTmpPath); + mkdirSync(projectTmpPath, { + recursive: true, + }); } if (appendPath) { projectTmpPath = path.join(projectTmpPath, appendPath); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index b2f7a14546f..ac89edf8c11 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -5,6 +5,8 @@ jest.mock('../Logger'); import { NoopLogger } from '../Logger'; import mockfs from 'mock-fs'; import { join } from 'node:path'; +import { MockLogger, MockProject } from './__utils__/MockProject'; +import { readFileSync, readdirSync } from 'node:fs'; describe('MessageProcessor with no config', () => { let messageProcessor: MessageProcessor; @@ -165,3 +167,69 @@ describe('MessageProcessor with no config', () => { // ); }); }); + +describe.only('project with simple config', () => { + afterEach(() => { + mockfs.restore(); + }); + it('caches files and schema with .graphql file config', async () => { + const project = new MockProject({ + files: [ + ['graphql.config.json', '{ "schema": "./schema.graphql" }'], + [ + 'schema.graphql', + 'type Query { foo: Foo }\n\ntype Foo { bar: String }', + ], + ['query.graphql', 'query { bar }'], + ], + }); + await project.lsp.handleInitializeRequest({ + rootPath: project.root, + rootUri: project.root, + capabilities: {}, + processId: 200, + workspaceFolders: null, + }); + await project.lsp.handleDidOpenOrSaveNotification({ + textDocument: { uri: project.uri('query.graphql') }, + }); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); + expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); + expect(Array.from(project.lsp._textDocumentCache)).toEqual([]); + }); + it('caches files and schema with a URL config', async () => { + const project = new MockProject({ + files: [ + [ + 'graphql.config.json', + '{ "schema": "https://rickandmortyapi.com/graphql" }', + ], + [ + 'schema.graphql', + 'type Query { foo: Foo }\n\ntype Foo { bar: String }', + ], + ['query.graphql', 'query { bar }'], + ], + }); + await project.lsp.handleInitializeRequest({ + rootPath: project.root, + rootUri: project.root, + capabilities: {}, + processId: 200, + workspaceFolders: null, + }); + await project.lsp.handleDidOpenOrSaveNotification({ + textDocument: { uri: project.uri('query.graphql') }, + }); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); + expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); + const file = readFileSync( + join( + '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql', + ), + ); + expect(file.toString('utf-8').length).toBeGreaterThan(0); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts new file mode 100644 index 00000000000..719e1f92a77 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -0,0 +1,78 @@ +import mockfs from 'mock-fs'; +import { MessageProcessor } from '../../MessageProcessor'; +import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; +import { URI } from 'vscode-uri'; + +export class MockLogger implements VSCodeLogger { + error = jest.fn(); + warn = jest.fn(); + info = jest.fn(); + log = jest.fn(); +} + +const defaultMocks = { + 'node_modules/parse-json': mockfs.load('node_modules/parse-json'), + 'node_modules/error-ex': mockfs.load('node_modules/error-ex'), + 'node_modules/is-arrayish': mockfs.load('node_modules/is-arrayish'), + 'node_modules/json-parse-even-better-errors': mockfs.load( + 'node_modules/json-parse-even-better-errors', + ), + 'node_modules/lines-and-columns': mockfs.load( + 'node_modules/lines-and-columns', + ), + 'node_modules/@babel/code-frame': mockfs.load( + 'node_modules/@babel/code-frame', + ), + 'node_modules/@babel/highlight': mockfs.load('node_modules/@babel/highlight'), + '/tmp/graphql-language-service/test/projects': mockfs.directory({ + mode: 0o777, + }), +}; + +export class MockProject { + private root: string; + private messageProcessor: MessageProcessor; + constructor({ + files = [], + root = '/tmp/test', + settings, + }: { + files: [filename: string, text: string][]; + root?: string; + settings?: [name: string, vale: any][]; + }) { + this.root = root; + const mockFiles = { ...defaultMocks }; + files.map(([filename, text]) => { + mockFiles[this.filePath(filename)] = text; + }); + mockfs(mockFiles); + this.messageProcessor = new MessageProcessor({ + connection: { + get workspace() { + return { + async getConfiguration() { + return settings; + }, + }; + }, + }, + logger: new MockLogger(), + loadConfigOptions: { rootDir: root }, + }); + } + public filePath(filename: string) { + return `${this.root}/${filename}`; + } + public uri(filename: string) { + return URI.file(this.filePath(filename)).toString(); + } + changeFile(filename: string, text: string) { + mockfs({ + [this.filePath(filename)]: text, + }); + } + get lsp() { + return this.messageProcessor; + } +} From 5ce0741935f2eca88619e1053192ca87a7b22213 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 4 Feb 2024 14:10:27 +0100 Subject: [PATCH 11/49] really exciting spec coverage --- .../src/__tests__/MessageProcessor.spec.ts | 44 ++++++++++++++----- .../src/__tests__/__utils__/MockProject.ts | 13 ++++-- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index ac89edf8c11..2dfa62188b4 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -5,8 +5,8 @@ jest.mock('../Logger'); import { NoopLogger } from '../Logger'; import mockfs from 'mock-fs'; import { join } from 'node:path'; -import { MockLogger, MockProject } from './__utils__/MockProject'; -import { readFileSync, readdirSync } from 'node:fs'; +import { MockProject } from './__utils__/MockProject'; +import { readFileSync } from 'node:fs'; describe('MessageProcessor with no config', () => { let messageProcessor: MessageProcessor; @@ -168,14 +168,17 @@ describe('MessageProcessor with no config', () => { }); }); -describe.only('project with simple config', () => { +describe('project with simple config', () => { afterEach(() => { mockfs.restore(); }); it('caches files and schema with .graphql file config', async () => { const project = new MockProject({ files: [ - ['graphql.config.json', '{ "schema": "./schema.graphql" }'], + [ + 'graphql.config.json', + '{ "schema": "./schema.graphql", "documents": "./**.graphql" }', + ], [ 'schema.graphql', 'type Query { foo: Foo }\n\ntype Foo { bar: String }', @@ -196,20 +199,20 @@ describe.only('project with simple config', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); - expect(Array.from(project.lsp._textDocumentCache)).toEqual([]); + expect(Array.from(project.lsp._textDocumentCache)[0][0]).toEqual( + project.uri('query.graphql'), + ); }); it('caches files and schema with a URL config', async () => { const project = new MockProject({ files: [ [ 'graphql.config.json', - '{ "schema": "https://rickandmortyapi.com/graphql" }', + '{ "schema": "https://rickandmortyapi.com/graphql", "documents": "./**.graphql" }', ], - [ - 'schema.graphql', - 'type Query { foo: Foo }\n\ntype Foo { bar: String }', - ], - ['query.graphql', 'query { bar }'], + + ['query.graphql', 'query { bar }'], + ['fragments.graphql', 'fragment Ep on Episode { created }'], ], }); await project.lsp.handleInitializeRequest({ @@ -222,6 +225,10 @@ describe.only('project with simple config', () => { await project.lsp.handleDidOpenOrSaveNotification({ textDocument: { uri: project.uri('query.graphql') }, }); + await project.lsp.handleDidChangeNotification({ + textDocument: { uri: project.uri('query.graphql'), version: 1 }, + contentChanges: [{ text: 'query { episodes { results { ...Ep } } }' }], + }); expect(project.lsp._logger.error).not.toHaveBeenCalled(); // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); @@ -231,5 +238,20 @@ describe.only('project with simple config', () => { ), ); expect(file.toString('utf-8').length).toBeGreaterThan(0); + const hover = await project.lsp.handleHoverRequest({ + position: { + character: 10, + line: 0, + }, + textDocument: { uri: project.uri('query.graphql') }, + }); + expect(project.lsp._textDocumentCache.size).toEqual(3); + + expect(hover.contents).toContain('Get the list of all episodes'); + const definitions = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, + position: { character: 33, line: 0 }, + }); + expect(definitions[0].uri).toEqual(project.uri('fragments.graphql')); }); }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index 719e1f92a77..ff1d149f719 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -24,9 +24,16 @@ const defaultMocks = { 'node_modules/@babel/code-frame', ), 'node_modules/@babel/highlight': mockfs.load('node_modules/@babel/highlight'), - '/tmp/graphql-language-service/test/projects': mockfs.directory({ - mode: 0o777, - }), + 'node_modules/jest-message-util': mockfs.load( + 'node_modules/jest-message-util', + ), + // 'node_modules/jest-message-util/node_modules/stack-util': mockfs.load( + // 'node_modules/jest-message-util/node_modules/stack-util', + // ), + // 'node_modules/stack-util': mockfs.load('node_modules/stack-util'), + // '/tmp/graphql-language-service/test/projects': mockfs.directory({ + // mode: 0o777, + // }), }; export class MockProject { From 12e94f53d091d8c2a099f22cdd6d1587031778fc Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 4 Feb 2024 21:20:56 +0100 Subject: [PATCH 12/49] more improvements and coverage --- .../src/GraphQLCache.ts | 23 +++--- .../src/MessageProcessor.ts | 45 +++++++----- .../src/__tests__/MessageProcessor.spec.ts | 61 ++++++++++++++-- .../src/__tests__/__utils__/MockProject.ts | 73 ++++++++++--------- 4 files changed, 132 insertions(+), 70 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index f7b043e5676..9fb18dd49af 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -529,6 +529,7 @@ export class GraphQLCache implements GraphQLCacheInterface { query, }; } catch { + console.log('parse error'); return { ast: null, query }; } }); @@ -664,14 +665,14 @@ export class GraphQLCache implements GraphQLCacheInterface { schemaCacheKey = schemaKey as string; // Maybe use cache - if (this._schemaMap.has(schemaCacheKey)) { - schema = this._schemaMap.get(schemaCacheKey); - if (schema) { - return queryHasExtensions - ? this._extendSchema(schema, schemaPath, schemaCacheKey) - : schema; - } - } + // if (this._schemaMap.has(schemaCacheKey)) { + // schema = this._schemaMap.get(schemaCacheKey); + // if (schema) { + // return queryHasExtensions + // ? this._extendSchema(schema, schemaPath, schemaCacheKey) + // : schema; + // } + // } // Read from disk schema = await projectConfig.getSchema(); @@ -691,9 +692,9 @@ export class GraphQLCache implements GraphQLCacheInterface { schema = this._extendSchema(schema, schemaPath, schemaCacheKey); } - if (schemaCacheKey) { - this._schemaMap.set(schemaCacheKey, schema); - } + // if (schemaCacheKey) { + // this._schemaMap.set(schemaCacheKey, schema); + // } return schema; }; diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 3bca4f1123e..fcdda7647c0 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -146,7 +146,7 @@ export class MessageProcessor { } if (!existsSync(this._tmpDirBase)) { - mkdirSync(this._tmpDirBase); + void mkdirSync(this._tmpDirBase); } } get connection(): Connection { @@ -671,9 +671,13 @@ export class MessageProcessor { ) { const { uri } = change; - const text = readFileSync(URI.parse(uri).fsPath, 'utf-8'); + const text = await readFile(URI.parse(uri).fsPath, 'utf-8'); const contents = this._parser(text, uri); - + await this._invalidateCache( + { uri, version: 0 }, + URI.parse(uri).fsPath, + contents, + ); await this._updateFragmentDefinition(uri, contents); await this._updateObjectTypeDefinition(uri, contents); @@ -720,16 +724,8 @@ export class MessageProcessor { } } if (change.type === FileChangeTypeKind.Deleted) { - await this._graphQLCache.updateFragmentDefinitionCache( - this._graphQLCache.getGraphQLConfig().dirpath, - change.uri, - false, - ); - await this._graphQLCache.updateObjectTypeDefinitionCache( - this._graphQLCache.getGraphQLConfig().dirpath, - change.uri, - false, - ); + await this._updateFragmentDefinition(change.uri, []); + await this._updateObjectTypeDefinition(change.uri, []); } }), ); @@ -1132,9 +1128,15 @@ export class MessageProcessor { uri: Uri, contents: CachedContent[], ): Promise { - const rootDir = this._graphQLCache.getGraphQLConfig().dirpath; - - await this._graphQLCache.updateFragmentDefinition(rootDir, uri, contents); + const project = this._graphQLCache.getProjectForFile(uri); + if (project) { + const cacheKey = this._graphQLCache._cacheKeyForProject(project); + await this._graphQLCache.updateFragmentDefinition( + cacheKey, + uri, + contents, + ); + } } private async _updateSchemaIfChanged( @@ -1189,9 +1191,16 @@ export class MessageProcessor { uri: Uri, contents: CachedContent[], ): Promise { - const rootDir = this._graphQLCache.getGraphQLConfig().dirpath; + const project = await this._graphQLCache.getProjectForFile(uri); + if (project) { + const cacheKey = this._graphQLCache._cacheKeyForProject(project); - await this._graphQLCache.updateObjectTypeDefinition(rootDir, uri, contents); + await this._graphQLCache.updateObjectTypeDefinition( + cacheKey, + uri, + contents, + ); + } } private _getCachedDocument(uri: string): CachedDocumentType | null { diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 2dfa62188b4..f5d08acc2cc 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -7,6 +7,9 @@ import mockfs from 'mock-fs'; import { join } from 'node:path'; import { MockProject } from './__utils__/MockProject'; import { readFileSync } from 'node:fs'; +import { FileChangeTypeKind } from 'graphql-language-service'; +import { FileChangeType } from 'vscode-languageserver'; +import { readFile } from 'node:fs/promises'; describe('MessageProcessor with no config', () => { let messageProcessor: MessageProcessor; @@ -168,11 +171,11 @@ describe('MessageProcessor with no config', () => { }); }); -describe('project with simple config', () => { +describe('project with simple config and graphql files', () => { afterEach(() => { mockfs.restore(); }); - it('caches files and schema with .graphql file config', async () => { + it.only('caches files and schema with .graphql file config', async () => { const project = new MockProject({ files: [ [ @@ -183,7 +186,8 @@ describe('project with simple config', () => { 'schema.graphql', 'type Query { foo: Foo }\n\ntype Foo { bar: String }', ], - ['query.graphql', 'query { bar }'], + ['query.graphql', 'query { bar ...B }'], + ['fragments.graphql', 'fragment B on Foo { bar }'], ], }); await project.lsp.handleInitializeRequest({ @@ -199,9 +203,41 @@ describe('project with simple config', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); - expect(Array.from(project.lsp._textDocumentCache)[0][0]).toEqual( - project.uri('query.graphql'), + // TODO: for some reason the cache result formats the graphql query?? + expect( + project.lsp._textDocumentCache.get(project.uri('query.graphql')) + .contents[0].query, + ).toContain('...B'); + const definitions = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(definitions[0].uri).toEqual(project.uri('schema.graphql')); + expect(JSON.parse(JSON.stringify(definitions[0].range.end))).toEqual({ + line: 2, + character: 24, + }); + // TODO: get mockfs working so we can change watched files. + // currently, when I run this, it removes the file entirely + project.changeFile( + 'schema.graphql', + 'type Query { foo: Foo }\n\n type Test { test: String }\n\n\n\n\ntype Foo { bad: Int, bar: String }', ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('schema.graphql'), type: FileChangeType.Changed }, + ], + }); + const definitionsAgain = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(definitionsAgain[0].uri).toEqual(project.uri('schema.graphql')); + // TODO: this should change when a watched file changes??? + expect(JSON.parse(JSON.stringify(definitions[0].range.end))).toEqual({ + line: 2, + character: 24, + }); }); it('caches files and schema with a URL config', async () => { const project = new MockProject({ @@ -212,7 +248,7 @@ describe('project with simple config', () => { ], ['query.graphql', 'query { bar }'], - ['fragments.graphql', 'fragment Ep on Episode { created }'], + ['fragments.graphql', 'fragment Ep on Episode {\n created \n}'], ], }); await project.lsp.handleInitializeRequest({ @@ -237,7 +273,7 @@ describe('project with simple config', () => { '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql', ), ); - expect(file.toString('utf-8').length).toBeGreaterThan(0); + expect(file.toString('utf-8').split('\n').length).toBeGreaterThan(10); const hover = await project.lsp.handleHoverRequest({ position: { character: 10, @@ -252,6 +288,17 @@ describe('project with simple config', () => { textDocument: { uri: project.uri('query.graphql') }, position: { character: 33, line: 0 }, }); + // ensure that fragment definitions work expect(definitions[0].uri).toEqual(project.uri('fragments.graphql')); + expect(JSON.parse(JSON.stringify(definitions[0].range))).toEqual({ + start: { + line: 0, + character: 0, + }, + end: { + line: 2, + character: 1, + }, + }); }); }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index ff1d149f719..59e7b8895af 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -10,50 +10,49 @@ export class MockLogger implements VSCodeLogger { log = jest.fn(); } -const defaultMocks = { - 'node_modules/parse-json': mockfs.load('node_modules/parse-json'), - 'node_modules/error-ex': mockfs.load('node_modules/error-ex'), - 'node_modules/is-arrayish': mockfs.load('node_modules/is-arrayish'), - 'node_modules/json-parse-even-better-errors': mockfs.load( - 'node_modules/json-parse-even-better-errors', - ), - 'node_modules/lines-and-columns': mockfs.load( - 'node_modules/lines-and-columns', - ), - 'node_modules/@babel/code-frame': mockfs.load( - 'node_modules/@babel/code-frame', - ), - 'node_modules/@babel/highlight': mockfs.load('node_modules/@babel/highlight'), - 'node_modules/jest-message-util': mockfs.load( - 'node_modules/jest-message-util', - ), - // 'node_modules/jest-message-util/node_modules/stack-util': mockfs.load( - // 'node_modules/jest-message-util/node_modules/stack-util', - // ), - // 'node_modules/stack-util': mockfs.load('node_modules/stack-util'), - // '/tmp/graphql-language-service/test/projects': mockfs.directory({ - // mode: 0o777, - // }), -}; +// when using mockfs with cosmic-config, a dynamic inline +// require of parse-json creates the necessity for loading in the actual +// modules to the mocked filesystem +const modules = [ + 'parse-json', + 'error-ex', + 'is-arrayish', + 'json-parse-even-better-errors', + 'lines-and-columns', + '@babel/code-frame', + '@babel/highlight', + // these i think are just required by jest when you console log from a test + 'jest-message-util', + 'stack-utils', + 'pretty-format', + 'ansi-regex', + 'js-tokens', + 'escape-string-regexp', +]; +const defaultMocks = modules.reduce((acc, module) => { + acc[`node_modules/${module}`] = mockfs.load(`node_modules/${module}`); + return acc; +}, {}); + +type Files = [filename: string, text: string][]; export class MockProject { private root: string; + private files: Files; private messageProcessor: MessageProcessor; constructor({ files = [], root = '/tmp/test', settings, }: { - files: [filename: string, text: string][]; + files: Files; root?: string; settings?: [name: string, vale: any][]; }) { this.root = root; - const mockFiles = { ...defaultMocks }; - files.map(([filename, text]) => { - mockFiles[this.filePath(filename)] = text; - }); - mockfs(mockFiles); + this.files = files; + + this.mockFiles(); this.messageProcessor = new MessageProcessor({ connection: { get workspace() { @@ -68,6 +67,13 @@ export class MockProject { loadConfigOptions: { rootDir: root }, }); } + private mockFiles() { + const mockFiles = { ...defaultMocks }; + this.files.map(([filename, text]) => { + mockFiles[this.filePath(filename)] = text; + }); + mockfs(mockFiles); + } public filePath(filename: string) { return `${this.root}/${filename}`; } @@ -75,9 +81,8 @@ export class MockProject { return URI.file(this.filePath(filename)).toString(); } changeFile(filename: string, text: string) { - mockfs({ - [this.filePath(filename)]: text, - }); + this.files.push([filename, text]); + this.mockFiles(); } get lsp() { return this.messageProcessor; From 0243ae25acc16eec335a989936131707c3898825 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Mon, 5 Feb 2024 02:08:25 +0100 Subject: [PATCH 13/49] refactor the whole integration suite --- .../src/GraphQLCache.ts | 71 +---- .../src/MessageProcessor.ts | 15 +- .../src/__tests__/MessageProcessor.spec.ts | 294 ++++++------------ .../src/__tests__/__utils__/MockProject.ts | 30 +- .../src/__tests__/__utils__/utils.ts | 4 + .../graphql-language-service/src/types.ts | 13 - 6 files changed, 147 insertions(+), 280 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 9fb18dd49af..5625c3061f8 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -424,32 +424,6 @@ export class GraphQLCache implements GraphQLCacheInterface { return patterns; }; - async _updateGraphQLFileListCache( - graphQLFileMap: Map, - metrics: { size: number; mtime: number }, - filePath: Uri, - exists: boolean, - ): Promise> { - const fileAndContent = exists - ? await this.promiseToReadGraphQLFile(filePath) - : null; - - const existingFile = graphQLFileMap.get(filePath); - - // 3 cases for the cache invalidation: create/modify/delete. - // For create/modify, swap the existing entry if available; - // otherwise, just push in the new entry created. - // For delete, check `exists` and splice the file out. - if (existingFile && !exists) { - graphQLFileMap.delete(filePath); - } else if (fileAndContent) { - const graphQLFileInfo = { ...fileAndContent, ...metrics }; - graphQLFileMap.set(filePath, graphQLFileInfo); - } - - return graphQLFileMap; - } - async updateFragmentDefinition( rootDir: Uri, filePath: Uri, @@ -490,32 +464,6 @@ export class GraphQLCache implements GraphQLCacheInterface { } } - async updateFragmentDefinitionCache( - rootDir: Uri, - filePath: Uri, - exists: boolean, - ): Promise { - const fileAndContent = exists - ? await this.promiseToReadGraphQLFile(filePath) - : null; - // In the case of fragment definitions, the cache could just map the - // definition name to the parsed ast, whether or not it existed - // previously. - // For delete, remove the entry from the set. - if (!exists) { - const cache = this._fragmentDefinitionsCache.get(rootDir); - if (cache) { - cache.delete(filePath); - } - } else if (fileAndContent?.queries) { - await this.updateFragmentDefinition( - rootDir, - filePath, - fileAndContent.queries, - ); - } - } - async updateObjectTypeDefinition( rootDir: Uri, filePath: Uri, @@ -664,18 +612,17 @@ export class GraphQLCache implements GraphQLCacheInterface { if (schemaPath && schemaKey) { schemaCacheKey = schemaKey as string; - // Maybe use cache - // if (this._schemaMap.has(schemaCacheKey)) { - // schema = this._schemaMap.get(schemaCacheKey); - // if (schema) { - // return queryHasExtensions - // ? this._extendSchema(schema, schemaPath, schemaCacheKey) - // : schema; - // } - // } - // Read from disk schema = await projectConfig.getSchema(); + + if (this._schemaMap.has(schemaCacheKey)) { + schema = this._schemaMap.get(schemaCacheKey); + if (schema) { + return queryHasExtensions + ? this._extendSchema(schema, schemaPath, schemaCacheKey) + : schema; + } + } } const customDirectives = projectConfig?.extensions?.customDirectives; diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index fcdda7647c0..9dd6164547f 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -314,7 +314,7 @@ export class MessageProcessor { async handleDidOpenOrSaveNotification( params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, - ): Promise { + ): Promise { /** * Initialize the LSP server when the first file is opened or saved, * so that we can access the user settings for config rootDir, etc @@ -327,7 +327,7 @@ export class MessageProcessor { // don't try to initialize again if we've already tried // and the graphql config file or package.json entry isn't even there if (this._isGraphQLConfigMissing === true && !isGraphQLConfigFile) { - return null; + return { uri: params.textDocument.uri, diagnostics: [] }; } // then initial call to update graphql config await this._updateGraphQLConfig(); @@ -360,13 +360,10 @@ export class MessageProcessor { contents = this._parser(text, uri); await this._invalidateCache(textDocument, uri, contents); - } else { - if (isGraphQLConfigFile) { - this._logger.info('updating graphql config'); - await this._updateGraphQLConfig(); - return { uri, diagnostics: [] }; - } - return null; + } else if (isGraphQLConfigFile) { + this._logger.info('updating graphql config'); + await this._updateGraphQLConfig(); + return { uri, diagnostics: [] }; } if (!this._graphQLCache) { return { uri, diagnostics }; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index f5d08acc2cc..6b89dd6a9fc 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -1,173 +1,71 @@ -import { MessageProcessor } from '../MessageProcessor'; - -jest.mock('../Logger'); - -import { NoopLogger } from '../Logger'; import mockfs from 'mock-fs'; import { join } from 'node:path'; -import { MockProject } from './__utils__/MockProject'; -import { readFileSync } from 'node:fs'; -import { FileChangeTypeKind } from 'graphql-language-service'; +import { MockFile, MockProject } from './__utils__/MockProject'; +// import { readFileSync } from 'node:fs'; import { FileChangeType } from 'vscode-languageserver'; +import { serializeRange } from './__utils__/utils'; import { readFile } from 'node:fs/promises'; -describe('MessageProcessor with no config', () => { - let messageProcessor: MessageProcessor; - const mockRoot = join('/tmp', 'test'); - let loggerSpy: jest.SpyInstance; - let mockProcessor; - - beforeEach(() => { - mockProcessor = (query: string, config?: string) => { - const items = { - 'query.graphql': query, - }; - if (config) { - items['graphql.config.js'] = config; - } - const files: Parameters[0] = { - [mockRoot]: mockfs.directory({ - items, - }), - 'node_modules/parse-json': mockfs.load('node_modules/parse-json'), - 'node_modules/error-ex': mockfs.load('node_modules/error-ex'), - 'node_modules/is-arrayish': mockfs.load('node_modules/is-arrayish'), - 'node_modules/json-parse-even-better-errors': mockfs.load( - 'node_modules/json-parse-even-better-errors', - ), - 'node_modules/lines-and-columns': mockfs.load( - 'node_modules/lines-and-columns', - ), - 'node_modules/@babel': mockfs.load('node_modules/@babel'), - }; - mockfs(files); - const logger = new NoopLogger(); - loggerSpy = jest.spyOn(logger, 'error'); - messageProcessor = new MessageProcessor({ - // @ts-ignore - connection: { - // @ts-ignore - get workspace() { - return { - async getConfiguration() { - return []; - }, - }; - }, - }, - logger, - graphqlFileExtensions: ['graphql'], - loadConfigOptions: { rootDir: mockRoot }, - }); - }; - }); +const defaultFiles = [ + ['query.graphql', 'query { bar ...B }'], + ['fragments.graphql', 'fragment B on Foo { bar }'], +] as MockFile[]; +const schemaFile: MockFile = [ + 'schema.graphql', + 'type Query { foo: Foo }\n\ntype Foo { bar: String }', +]; +describe('MessageProcessor with no config', () => { afterEach(() => { mockfs.restore(); }); it('fails to initialize with empty config file', async () => { - mockProcessor('query { foo }', ''); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( + const project = new MockProject({ + files: [...defaultFiles, ['graphql.config.json', '']], + }); + await project.init(); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + expect(project.lsp._logger.info).toHaveBeenCalledTimes(1); + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); + expect(project.lsp._logger.error).toHaveBeenCalledWith( expect.stringMatching( /GraphQL Config file is not available in the provided config directory/, ), ); }); it('fails to initialize with no config file present', async () => { - mockProcessor('query { foo }'); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, + const project = new MockProject({ + files: [...defaultFiles], }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( + await project.init(); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); + expect(project.lsp._logger.error).toHaveBeenCalledWith( expect.stringMatching( /GraphQL Config file is not available in the provided config directory/, ), ); }); it('initializes when presented with a valid config later', async () => { - mockProcessor('query { foo }'); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, + const project = new MockProject({ + files: [...defaultFiles], }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(loggerSpy).toHaveBeenCalledTimes(1); + await project.init(); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); - // todo: get mockfs working with in-test file changes - // mockfs({ - // [mockRoot]: mockfs.directory({ - // mode: 0o755, - // items: { - // 'schema.graphql': - // 'type Query { foo: String }\nschema { query: Query }', - // 'graphql.config.js': mockfs.file({ - // content: 'module.exports = { schema: "schema.graphql" };', - // mode: 0o644, - // }), - // 'query.graphql': 'query { foo }', - // }, - // }), - // }); - // // console.log(readdirSync(`${mockRoot}`)); - // await messageProcessor.handleDidOpenOrSaveNotification({ - // textDocument: { - // text: 'module.exports = { schema: "schema.graphql" }', - // uri: `${mockRoot}/graphql.config.js`, - // version: 2, - // }, - // }); - - // expect(messageProcessor._isGraphQLConfigMissing).toEqual(false); - - // expect(loggerSpy).toHaveBeenCalledWith( - // expect.stringMatching( - // /GraphQL Config file is not available in the provided config directory/, - // ), - // ); + project.changeFile( + 'graphql.config.json', + '{ "schema": "./schema.graphql" }', + ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('schema.graphql'), type: FileChangeType.Changed }, + ], + }); }); }); @@ -175,92 +73,105 @@ describe('project with simple config and graphql files', () => { afterEach(() => { mockfs.restore(); }); - it.only('caches files and schema with .graphql file config', async () => { + it('caches files and schema with .graphql file config, and the schema updates with watched file changes', async () => { const project = new MockProject({ files: [ [ 'graphql.config.json', '{ "schema": "./schema.graphql", "documents": "./**.graphql" }', ], - [ - 'schema.graphql', - 'type Query { foo: Foo }\n\ntype Foo { bar: String }', - ], - ['query.graphql', 'query { bar ...B }'], - ['fragments.graphql', 'fragment B on Foo { bar }'], + ...defaultFiles, + schemaFile, ], }); - await project.lsp.handleInitializeRequest({ - rootPath: project.root, - rootUri: project.root, - capabilities: {}, - processId: 200, - workspaceFolders: null, - }); - await project.lsp.handleDidOpenOrSaveNotification({ - textDocument: { uri: project.uri('query.graphql') }, - }); + await project.init('query.graphql'); expect(project.lsp._logger.error).not.toHaveBeenCalled(); // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); // TODO: for some reason the cache result formats the graphql query?? + const docCache = project.lsp._textDocumentCache; expect( - project.lsp._textDocumentCache.get(project.uri('query.graphql')) - .contents[0].query, + docCache.get(project.uri('query.graphql'))!.contents[0].query, ).toContain('...B'); const definitions = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('fragments.graphql') }, position: { character: 16, line: 0 }, }); expect(definitions[0].uri).toEqual(project.uri('schema.graphql')); - expect(JSON.parse(JSON.stringify(definitions[0].range.end))).toEqual({ + + expect(serializeRange(definitions[0].range).end).toEqual({ line: 2, character: 24, }); - // TODO: get mockfs working so we can change watched files. - // currently, when I run this, it removes the file entirely + + const definitionsAgain = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(definitionsAgain[0].uri).toEqual(project.uri('schema.graphql')); + + expect(serializeRange(definitionsAgain[0].range).end).toEqual({ + line: 2, + character: 24, + }); + // change the file to make the fragment invalid project.changeFile( 'schema.graphql', - 'type Query { foo: Foo }\n\n type Test { test: String }\n\n\n\n\ntype Foo { bad: Int, bar: String }', + // now Foo has a bad field, the fragment should be invalid + 'type Query { foo: Foo }\n\n type Test { test: String }\n\n\n\n\ntype Foo { bad: Int }', ); await project.lsp.handleWatchedFilesChangedNotification({ changes: [ { uri: project.uri('schema.graphql'), type: FileChangeType.Changed }, ], }); - const definitionsAgain = await project.lsp.handleDefinitionRequest({ - textDocument: { uri: project.uri('fragments.graphql') }, + const typeCache = + project.lsp._graphQLCache._typeDefinitionsCache.get('/tmp/test-default'); + + expect(typeCache?.get('Test')?.definition.name.value).toEqual('Test'); + // TODO: this fragment should now be invalid + // const result = await project.lsp.handleDidOpenOrSaveNotification({ + // textDocument: { uri: project.uri('fragments.graphql') }, + // }); + // expect(result.diagnostics).toEqual([]); + + project.changeFile( + 'fragments.graphql', + 'fragment B on Foo { bear }\n\nfragment A on Foo { bar }', + ); + + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('fragments.graphql'), type: FileChangeType.Changed }, + ], + }); + const fragCache = + project.lsp._graphQLCache._fragmentDefinitionsCache.get( + '/tmp/test-default', + ); + expect(fragCache?.get('A')?.definition.name.value).toEqual('A'); + // TODO: get this working + const definitionsThrice = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, position: { character: 16, line: 0 }, }); - expect(definitionsAgain[0].uri).toEqual(project.uri('schema.graphql')); + expect(definitionsThrice[0].uri).toEqual(project.uri('fragments.graphql')); // TODO: this should change when a watched file changes??? - expect(JSON.parse(JSON.stringify(definitions[0].range.end))).toEqual({ - line: 2, - character: 24, - }); }); it('caches files and schema with a URL config', async () => { const project = new MockProject({ files: [ + ['query.graphql', 'query { bar }'], + ['fragments.graphql', 'fragment Ep on Episode {\n created \n}'], [ 'graphql.config.json', '{ "schema": "https://rickandmortyapi.com/graphql", "documents": "./**.graphql" }', ], - - ['query.graphql', 'query { bar }'], - ['fragments.graphql', 'fragment Ep on Episode {\n created \n}'], ], }); - await project.lsp.handleInitializeRequest({ - rootPath: project.root, - rootUri: project.root, - capabilities: {}, - processId: 200, - workspaceFolders: null, - }); - await project.lsp.handleDidOpenOrSaveNotification({ - textDocument: { uri: project.uri('query.graphql') }, - }); + + await project.init('query.graphql'); + await project.lsp.handleDidChangeNotification({ textDocument: { uri: project.uri('query.graphql'), version: 1 }, contentChanges: [{ text: 'query { episodes { results { ...Ep } } }' }], @@ -268,12 +179,13 @@ describe('project with simple config and graphql files', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); - const file = readFileSync( + const file = await readFile( join( '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql', ), + { encoding: 'utf-8' }, ); - expect(file.toString('utf-8').split('\n').length).toBeGreaterThan(10); + expect(file.split('\n').length).toBeGreaterThan(10); const hover = await project.lsp.handleHoverRequest({ position: { character: 10, @@ -290,7 +202,7 @@ describe('project with simple config and graphql files', () => { }); // ensure that fragment definitions work expect(definitions[0].uri).toEqual(project.uri('fragments.graphql')); - expect(JSON.parse(JSON.stringify(definitions[0].range))).toEqual({ + expect(serializeRange(definitions[0].range)).toEqual({ start: { line: 0, character: 0, diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index 59e7b8895af..f1511c11df6 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -3,6 +3,8 @@ import { MessageProcessor } from '../../MessageProcessor'; import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import { URI } from 'vscode-uri'; +export type MockFile = [filename: string, text: string]; + export class MockLogger implements VSCodeLogger { error = jest.fn(); warn = jest.fn(); @@ -28,17 +30,19 @@ const modules = [ 'ansi-regex', 'js-tokens', 'escape-string-regexp', + 'jest-worker', ]; const defaultMocks = modules.reduce((acc, module) => { acc[`node_modules/${module}`] = mockfs.load(`node_modules/${module}`); return acc; }, {}); -type Files = [filename: string, text: string][]; +type File = [filename: string, text: string]; +type Files = File[]; export class MockProject { private root: string; - private files: Files; + private fileCache: Map; private messageProcessor: MessageProcessor; constructor({ files = [], @@ -50,7 +54,7 @@ export class MockProject { settings?: [name: string, vale: any][]; }) { this.root = root; - this.files = files; + this.fileCache = new Map(files); this.mockFiles(); this.messageProcessor = new MessageProcessor({ @@ -67,9 +71,25 @@ export class MockProject { loadConfigOptions: { rootDir: root }, }); } + public async init(filename?: string, fileText?: string) { + await this.lsp.handleInitializeRequest({ + rootPath: this.root, + rootUri: this.root, + capabilities: {}, + processId: 200, + workspaceFolders: null, + }); + return this.lsp.handleDidOpenOrSaveNotification({ + textDocument: { + uri: this.uri(filename || this.uri('query.graphql')), + version: 1, + text: this.fileCache.get('query.graphql') || fileText, + }, + }); + } private mockFiles() { const mockFiles = { ...defaultMocks }; - this.files.map(([filename, text]) => { + Array.from(this.fileCache).map(([filename, text]) => { mockFiles[this.filePath(filename)] = text; }); mockfs(mockFiles); @@ -81,7 +101,7 @@ export class MockProject { return URI.file(this.filePath(filename)).toString(); } changeFile(filename: string, text: string) { - this.files.push([filename, text]); + this.fileCache.set(filename, text); this.mockFiles(); } get lsp() { diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts new file mode 100644 index 00000000000..4ad1eff2c26 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts @@ -0,0 +1,4 @@ +import { Range } from 'vscode-languageserver'; + +export const serializeRange = (range: Range) => + JSON.parse(JSON.stringify(range)); diff --git a/packages/graphql-language-service/src/types.ts b/packages/graphql-language-service/src/types.ts index 6e4d8c47626..7ed008290f4 100644 --- a/packages/graphql-language-service/src/types.ts +++ b/packages/graphql-language-service/src/types.ts @@ -70,12 +70,6 @@ export interface GraphQLCache { contents: CachedContent[], ) => Promise; - updateObjectTypeDefinitionCache: ( - rootDir: Uri, - filePath: Uri, - exists: boolean, - ) => Promise; - getFragmentDependencies: ( query: string, fragmentDefinitions: Maybe>, @@ -95,13 +89,6 @@ export interface GraphQLCache { filePath: Uri, contents: CachedContent[], ) => Promise; - - updateFragmentDefinitionCache: ( - rootDir: Uri, - filePath: Uri, - exists: boolean, - ) => Promise; - getSchema: ( appName?: string, queryHasExtensions?: boolean, From ee087b268f56611f1495881efcee037f77085d7c Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 11 Feb 2024 22:05:25 +0100 Subject: [PATCH 14/49] get set up for a local schema lifecycle --- .github/workflows/pr.yml | 28 +- jest.config.base.js | 2 +- jest.config.js | 1 + package.json | 2 + packages/graphiql/test/beforeDevServer.js | 9 +- packages/graphiql/test/schema.js | 240 ++++++++++-------- .../src/GraphQLCache.ts | 36 +-- .../src/MessageProcessor.ts | 57 +++-- .../src/__tests__/MessageProcessor.spec.ts | 118 ++++++--- .../__tests__/__utils__/runSchemaServer.ts | 0 yarn.lock | 26 +- 11 files changed, 320 insertions(+), 199 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b6c35bd1a43..ab78129cd17 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -86,7 +86,7 @@ jobs: key: modules-${{ github.sha }} - run: yarn pretty-check - jest: + jest-unit: name: Jest Unit Tests runs-on: ubuntu-latest needs: [install] @@ -101,7 +101,29 @@ jobs: path: | **/node_modules key: modules-${{ github.sha }} - - run: yarn test --coverage + - run: yarn test:unit --coverage + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage/lcov.info + fail_ci_if_error: true + verbose: true + jest-spec: + name: Jest Integration Tests + runs-on: ubuntu-latest + needs: [install] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - id: cache-modules + uses: actions/cache@v3 + with: + path: | + **/node_modules + key: modules-${{ github.sha }} + - run: yarn test:spec --coverage - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -109,7 +131,7 @@ jobs: fail_ci_if_error: true verbose: true - vitest: +vitest: name: Vitest Unit Tests runs-on: ubuntu-latest needs: [build] diff --git a/jest.config.base.js b/jest.config.base.js index 15e87eda8f8..6a401259a83 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -33,7 +33,7 @@ module.exports = (dir, env = 'jsdom') => { // because of the svelte compiler's export patterns i guess? 'svelte/compiler': `${__dirname}/node_modules/svelte/compiler.cjs`, }, - testMatch: ['**/*[-.](spec|test).[jt]s?(x)', '!**/cypress/**'], + testMatch: ['**/*[-.](test|spec).[jt]s?(x)', '!**/cypress/**'], testEnvironment: env, testPathIgnorePatterns: ['node_modules', 'dist', 'cypress'], collectCoverageFrom: ['**/src/**/*.{js,jsx,ts,tsx}'], diff --git a/jest.config.js b/jest.config.js index 3ef34f68be1..5e22fa5dd70 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ module.exports = { + ...require('./jest.config.base.js')(__dirname), projects: ['/packages/*/jest.config.js'], }; diff --git a/package.json b/package.json index 21399374029..dfceeb3d3a6 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,8 @@ "test:ci": "yarn build && jest --coverage && yarn vitest", "test:coverage": "yarn jest --coverage", "test:watch": "yarn jest --watch", + "test:spec": "TEST_ENV=spec yarn jest --testPathIgnorePatterns test.ts", + "test:unit": "yarn jest --testPathIgnorePatterns spec.ts", "tsc": "tsc --build", "vitest": "yarn wsrun -p -m test", "wsrun:noexamples": "wsrun --exclude-missing --exclude example-monaco-graphql-react-vite --exclude example-monaco-graphql-nextjs --exclude example-monaco-graphql-webpack --exclude example-graphiql-webpack" diff --git a/packages/graphiql/test/beforeDevServer.js b/packages/graphiql/test/beforeDevServer.js index d386ae47922..77e868c4057 100644 --- a/packages/graphiql/test/beforeDevServer.js +++ b/packages/graphiql/test/beforeDevServer.js @@ -9,13 +9,20 @@ const express = require('express'); const path = require('node:path'); const { createHandler } = require('graphql-http/lib/use/express'); const schema = require('./schema'); -const { schema: badSchema } = require('./bad-schema'); +const { schema: badSchema, changedSchema } = require('./bad-schema'); module.exports = function beforeDevServer(app, _server, _compiler) { // GraphQL Server app.post('/graphql', createHandler({ schema })); app.get('/graphql', createHandler({ schema })); + app.post('/changed/graphql', createHandler({ schema: changedSchema })); + + app.post('/bad/graphql', (_req, res, next) => { + res.json({ data: badSchema }); + next(); + }); + app.post('/bad/graphql', (_req, res, next) => { res.json({ data: badSchema }); next(); diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js index fcd648096f1..61612b13c16 100644 --- a/packages/graphiql/test/schema.js +++ b/packages/graphiql/test/schema.js @@ -230,119 +230,115 @@ And we have a cool logo: ![](/images/logo.svg) `.trim(); -const TestType = new GraphQLObjectType({ - name: 'Test', - description: 'Test type for testing\n New line works', - fields: () => ({ - test: { - type: TestType, - description: '`test` field from `Test` type.', - resolve: () => ({}), - }, - deferrable: { - type: DeferrableObject, - resolve: () => ({}), +const defaultFields = { + test: { + type: TestType, + description: '`test` field from `Test` type.', + resolve: () => ({}), + }, + deferrable: { + type: DeferrableObject, + resolve: () => ({}), + }, + streamable: { + type: new GraphQLList(Greeting), + args: { + delay: delayArgument(300), }, - streamable: { - type: new GraphQLList(Greeting), - args: { - delay: delayArgument(300), - }, - resolve: async function* sayHiInSomeLanguages(_value, args) { - let i = 0; - for (const hi of [ - 'Hi', - '你好', - 'Hola', - 'أهلاً', - 'Bonjour', - 'سلام', - '안녕', - 'Ciao', - 'हेलो', - 'Здорово', - ]) { - if (i > 2) { - await sleep(args.delay); - } - i++; - yield { text: hi }; + resolve: async function* sayHiInSomeLanguages(_value, args) { + let i = 0; + for (const hi of [ + 'Hi', + '你好', + 'Hola', + 'أهلاً', + 'Bonjour', + 'سلام', + '안녕', + 'Ciao', + 'हेलो', + 'Здорово', + ]) { + if (i > 2) { + await sleep(args.delay); } - }, - }, - person: { - type: Person, - resolve: () => ({ name: 'Mark' }), - }, - longDescriptionType: { - type: TestType, - description: longDescription, - resolve: () => ({}), + i++; + yield { text: hi }; + } }, - union: { - type: TestUnion, - resolve: () => ({}), - }, - id: { - type: GraphQLID, - description: 'id field from Test type.', - resolve: () => 'abc123', - }, - isTest: { - type: GraphQLBoolean, - description: 'Is this a test schema? Sure it is.', - resolve: () => true, - }, - image: { - type: GraphQLString, - description: 'field that returns an image URI.', - resolve: () => '/images/logo.svg', - }, - deprecatedField: { - type: TestType, - description: 'This field is an example of a deprecated field', - deprecationReason: 'No longer in use, try `test` instead.', - }, - alsoDeprecated: { - type: TestType, - description: - 'This field is an example of a deprecated field with markdown in its deprecation reason', - deprecationReason: longDescription, + }, + person: { + type: Person, + resolve: () => ({ name: 'Mark' }), + }, + longDescriptionType: { + type: TestType, + description: longDescription, + resolve: () => ({}), + }, + union: { + type: TestUnion, + resolve: () => ({}), + }, + id: { + type: GraphQLID, + description: 'id field from Test type.', + resolve: () => 'abc123', + }, + isTest: { + type: GraphQLBoolean, + description: 'Is this a test schema? Sure it is.', + resolve: () => true, + }, + image: { + type: GraphQLString, + description: 'field that returns an image URI.', + resolve: () => '/images/logo.svg', + }, + deprecatedField: { + type: TestType, + description: 'This field is an example of a deprecated field', + deprecationReason: 'No longer in use, try `test` instead.', + }, + alsoDeprecated: { + type: TestType, + description: + 'This field is an example of a deprecated field with markdown in its deprecation reason', + deprecationReason: longDescription, + }, + hasArgs: { + type: GraphQLString, + resolve(_value, args) { + return JSON.stringify(args); }, - hasArgs: { - type: GraphQLString, - resolve(_value, args) { - return JSON.stringify(args); + args: { + string: { type: GraphQLString, description: 'A string' }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + defaultValue: { + type: GraphQLString, + defaultValue: 'test default value', }, - args: { - string: { type: GraphQLString, description: 'A string' }, - int: { type: GraphQLInt }, - float: { type: GraphQLFloat }, - boolean: { type: GraphQLBoolean }, - id: { type: GraphQLID }, - enum: { type: TestEnum }, - object: { type: TestInputObject }, - defaultValue: { - type: GraphQLString, - defaultValue: 'test default value', - }, - // List - listString: { type: new GraphQLList(GraphQLString) }, - listInt: { type: new GraphQLList(GraphQLInt) }, - listFloat: { type: new GraphQLList(GraphQLFloat) }, - listBoolean: { type: new GraphQLList(GraphQLBoolean) }, - listID: { type: new GraphQLList(GraphQLID) }, - listEnum: { type: new GraphQLList(TestEnum) }, - listObject: { type: new GraphQLList(TestInputObject) }, - deprecatedArg: { - type: GraphQLString, - deprecationReason: 'deprecated argument', - description: 'Hello!', - }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, + deprecatedArg: { + type: GraphQLString, + deprecationReason: 'deprecated argument', + description: 'Hello!', }, }, - }), -}); + }, +}; const TestMutationType = new GraphQLObjectType({ name: 'MutationType', @@ -381,6 +377,16 @@ const TestSubscriptionType = new GraphQLObjectType({ }, }); +const getTestType = (fields = defaultFields) => { + return new GraphQLObjectType({ + name: 'Test', + description: 'Test type for testing\n New line works', + fields: () => fields, + }); +}; + +const TestType = getTestType(); + const myTestSchema = new GraphQLSchema({ query: TestType, mutation: TestMutationType, @@ -388,4 +394,26 @@ const myTestSchema = new GraphQLSchema({ description: 'This is a test schema for GraphiQL', }); +const ChangedTestType = getTestType({ + ...defaultFields, + newField: { + type: TestType, + resolve: () => ({}), + }, + isTest: { + type: GraphQLString, + description: 'Is this a test schema? Sure it is.', + resolve: () => true, + }, +}); + +const myChangedTestSchema = new GraphQLSchema({ + query: ChangedTestType, + mutation: TestMutationType, + subscription: TestSubscriptionType, + description: 'This is a changed test schema for GraphiQL', +}); + module.exports = myTestSchema; +module.exports.changedSchema = myChangedTestSchema; +module.exports.defaultFields = defaultFields; diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 5625c3061f8..83ae8f3c94c 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -130,7 +130,15 @@ export class GraphQLCache implements GraphQLCacheInterface { getProjectForFile = (uri: string): GraphQLProjectConfig | void => { try { - return this._graphQLConfig.getProjectForFile(URI.parse(uri).fsPath); + const project = this._graphQLConfig.getProjectForFile( + URI.parse(uri).fsPath, + ); + if (!project.documents) { + this._logger.warn( + `No documents configured for project ${project.name}. Many features will not work correctly.`, + ); + } + return project; } catch (err) { this._logger.error( `there was an error loading the project config for this file ${err}`, @@ -505,32 +513,6 @@ export class GraphQLCache implements GraphQLCacheInterface { } } - async updateObjectTypeDefinitionCache( - rootDir: Uri, - filePath: Uri, - exists: boolean, - ): Promise { - const fileAndContent = exists - ? await this.promiseToReadGraphQLFile(filePath) - : null; - // In the case of type definitions, the cache could just map the - // definition name to the parsed ast, whether or not it existed - // previously. - // For delete, remove the entry from the set. - if (!exists) { - const cache = this._typeDefinitionsCache.get(rootDir); - if (cache) { - cache.delete(filePath); - } - } else if (fileAndContent?.queries) { - await this.updateObjectTypeDefinition( - rootDir, - filePath, - fileAndContent.queries, - ); - } - } - _extendSchema( schema: GraphQLSchema, schemaPath: string | null, diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 9dd6164547f..9851683891e 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -7,7 +7,7 @@ * */ -import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; +import { existsSync, writeFileSync, mkdirSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; import { URI } from 'vscode-uri'; @@ -71,6 +71,7 @@ import { import type { LoadConfigOptions } from './types'; import { DEFAULT_SUPPORTED_EXTENSIONS, + DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, SupportedExtensionsEnum, } from './constants'; import { NoopLogger, Logger } from './Logger'; @@ -242,12 +243,13 @@ export class MessageProcessor { this._graphQLCache, this._logger, ); - if (this._graphQLConfig || this._graphQLCache?.getGraphQLConfig) { - const config = - this._graphQLConfig ?? this._graphQLCache.getGraphQLConfig(); + const config = this._graphQLCache.getGraphQLConfig(); + if (config) { + this._graphQLConfig = config; await this._cacheAllProjectFiles(config); + this._isInitialized = true; + this._isGraphQLConfigMissing = false; } - this._isInitialized = true; } catch (err) { this._handleConfigError({ err }); } @@ -939,19 +941,28 @@ export class MessageProcessor { fileUri: UnnormalizedTypeDefPointer, project: GraphQLProjectConfig, ) { - const uri = fileUri.toString(); - - const isFileUri = existsSync(uri); - let version = 1; - if (isFileUri) { - const schemaUri = URI.file(path.join(project.dirpath, uri)).toString(); - const schemaDocument = this._getCachedDocument(schemaUri); - - if (schemaDocument) { - version = schemaDocument.version++; + try { + // const parsedUri = URI.file(fileUri.toString()); + // console.log(readdirSync(project.dirpath), fileUri.toString()); + // @ts-expect-error + const matches = await glob(fileUri, { + cwd: project.dirpath, + absolute: true, + }); + const uri = matches[0]; + let version = 1; + if (uri) { + const schemaUri = URI.file(uri).toString(); + const schemaDocument = this._getCachedDocument(schemaUri); + + if (schemaDocument) { + version = schemaDocument.version++; + } + const schemaText = await readFile(uri, 'utf8'); + await this._cacheSchemaText(schemaUri, schemaText, version); } - const schemaText = readFileSync(uri, 'utf8'); - await this._cacheSchemaText(schemaUri, schemaText, version); + } catch { + // this._logger.error(String(err)); } } private _getTmpProjectPath( @@ -998,9 +1009,15 @@ export class MessageProcessor { this?._settings?.cacheSchemaFileForLookup ?? true; const unwrappedSchema = this._unwrapProjectSchema(project); - const sdlOnly = unwrappedSchema.every( - schemaEntry => - schemaEntry.endsWith('.graphql') || schemaEntry.endsWith('.gql'), + const allExtensions = [ + ...DEFAULT_SUPPORTED_EXTENSIONS, + ...DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, + ]; + // only local schema lookups if all of the schema entries are local files that we can resolve + const sdlOnly = unwrappedSchema.every(schemaEntry => + allExtensions.some( + ext => !schemaEntry.startsWith('http') && schemaEntry.endsWith(ext), + ), ); // if we are caching the config schema, and it isn't a .graphql file, cache it if (cacheSchemaFileForLookup && !sdlOnly) { diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 6b89dd6a9fc..2998ca970cf 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -5,6 +5,7 @@ import { MockFile, MockProject } from './__utils__/MockProject'; import { FileChangeType } from 'vscode-languageserver'; import { serializeRange } from './__utils__/utils'; import { readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; const defaultFiles = [ ['query.graphql', 'query { bar ...B }'], @@ -15,6 +16,9 @@ const schemaFile: MockFile = [ 'type Query { foo: Foo }\n\ntype Foo { bar: String }', ]; +const genSchemaPath = + '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql'; + describe('MessageProcessor with no config', () => { afterEach(() => { mockfs.restore(); @@ -61,11 +65,16 @@ describe('MessageProcessor with no config', () => { 'graphql.config.json', '{ "schema": "./schema.graphql" }', ); - await project.lsp.handleWatchedFilesChangedNotification({ - changes: [ - { uri: project.uri('schema.graphql'), type: FileChangeType.Changed }, - ], + // TODO: this should work for on watched file changes as well! + await project.lsp.handleDidOpenOrSaveNotification({ + textDocument: { + uri: project.uri('graphql.config.json'), + }, }); + expect(project.lsp._isInitialized).toEqual(true); + expect(project.lsp._isGraphQLConfigMissing).toEqual(false); + + expect(project.lsp._graphQLCache).toBeDefined(); }); }); @@ -76,12 +85,12 @@ describe('project with simple config and graphql files', () => { it('caches files and schema with .graphql file config, and the schema updates with watched file changes', async () => { const project = new MockProject({ files: [ + schemaFile, [ 'graphql.config.json', '{ "schema": "./schema.graphql", "documents": "./**.graphql" }', ], ...defaultFiles, - schemaFile, ], }); await project.init('query.graphql'); @@ -93,32 +102,40 @@ describe('project with simple config and graphql files', () => { expect( docCache.get(project.uri('query.graphql'))!.contents[0].query, ).toContain('...B'); - const definitions = await project.lsp.handleDefinitionRequest({ + const schemaDefinitions = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('fragments.graphql') }, position: { character: 16, line: 0 }, }); - expect(definitions[0].uri).toEqual(project.uri('schema.graphql')); + expect(schemaDefinitions[0].uri).toEqual(project.uri('schema.graphql')); - expect(serializeRange(definitions[0].range).end).toEqual({ + expect(serializeRange(schemaDefinitions[0].range).end).toEqual({ line: 2, character: 24, }); - const definitionsAgain = await project.lsp.handleDefinitionRequest({ - textDocument: { uri: project.uri('fragments.graphql') }, + // query definition request of fragment name jumps to the fragment definition + const firstQueryDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, position: { character: 16, line: 0 }, }); - expect(definitionsAgain[0].uri).toEqual(project.uri('schema.graphql')); - - expect(serializeRange(definitionsAgain[0].range).end).toEqual({ - line: 2, - character: 24, + expect(firstQueryDefRequest[0].uri).toEqual( + project.uri('fragments.graphql'), + ); + expect(serializeRange(firstQueryDefRequest[0].range)).toEqual({ + start: { + line: 0, + character: 0, + }, + end: { + line: 0, + character: 25, + }, }); // change the file to make the fragment invalid project.changeFile( 'schema.graphql', // now Foo has a bad field, the fragment should be invalid - 'type Query { foo: Foo }\n\n type Test { test: String }\n\n\n\n\ntype Foo { bad: Int }', + 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\ntype Foo { bad: Int }', ); await project.lsp.handleWatchedFilesChangedNotification({ changes: [ @@ -130,33 +147,64 @@ describe('project with simple config and graphql files', () => { expect(typeCache?.get('Test')?.definition.name.value).toEqual('Test'); // TODO: this fragment should now be invalid - // const result = await project.lsp.handleDidOpenOrSaveNotification({ - // textDocument: { uri: project.uri('fragments.graphql') }, - // }); - // expect(result.diagnostics).toEqual([]); - + const result = await project.lsp.handleDidOpenOrSaveNotification({ + textDocument: { uri: project.uri('fragments.graphql') }, + }); + expect(result.diagnostics).toEqual([]); + const generatedFile = existsSync(join(genSchemaPath)); + // this generated file should not exist because the schema is local! + expect(generatedFile).toEqual(false); + // simulating codegen project.changeFile( 'fragments.graphql', - 'fragment B on Foo { bear }\n\nfragment A on Foo { bar }', + 'fragment A on Foo { bar }\n\nfragment B on Test { test }', ); - await project.lsp.handleWatchedFilesChangedNotification({ changes: [ { uri: project.uri('fragments.graphql'), type: FileChangeType.Changed }, ], }); + + // TODO: this interface should maybe not be tested here but in unit tests const fragCache = project.lsp._graphQLCache._fragmentDefinitionsCache.get( '/tmp/test-default', ); expect(fragCache?.get('A')?.definition.name.value).toEqual('A'); - // TODO: get this working - const definitionsThrice = await project.lsp.handleDefinitionRequest({ + expect(fragCache?.get('B')?.definition.name.value).toEqual('B'); + + // on the second request, the position has changed + const secondQueryDefRequest = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('query.graphql') }, position: { character: 16, line: 0 }, }); - expect(definitionsThrice[0].uri).toEqual(project.uri('fragments.graphql')); - // TODO: this should change when a watched file changes??? + expect(secondQueryDefRequest[0].uri).toEqual( + project.uri('fragments.graphql'), + ); + expect(serializeRange(secondQueryDefRequest[0].range)).toEqual({ + start: { + line: 2, + character: 0, + }, + end: { + line: 2, + character: 27, + }, + }); + // definitions request for fragments jumps to a different place in schema.graphql now + const schemaDefinitionsAgain = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(schemaDefinitionsAgain[0].uri).toEqual( + project.uri('schema.graphql'), + ); + + expect(serializeRange(schemaDefinitionsAgain[0].range).end).toEqual({ + line: 7, + character: 21, + }); + // TODO: the position should change when a watched file changes??? }); it('caches files and schema with a URL config', async () => { const project = new MockProject({ @@ -172,19 +220,19 @@ describe('project with simple config and graphql files', () => { await project.init('query.graphql'); - await project.lsp.handleDidChangeNotification({ + const changeParams = await project.lsp.handleDidChangeNotification({ textDocument: { uri: project.uri('query.graphql'), version: 1 }, - contentChanges: [{ text: 'query { episodes { results { ...Ep } } }' }], + contentChanges: [ + { text: 'query { episodes { results { ...Ep, nop } } }' }, + ], }); + expect(changeParams?.diagnostics[0].message).toEqual( + 'Cannot query field "nop" on type "Episode".', + ); expect(project.lsp._logger.error).not.toHaveBeenCalled(); // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); - const file = await readFile( - join( - '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql', - ), - { encoding: 'utf-8' }, - ); + const file = await readFile(join(genSchemaPath), { encoding: 'utf-8' }); expect(file.split('\n').length).toBeGreaterThan(10); const hover = await project.lsp.handleHoverRequest({ position: { diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/yarn.lock b/yarn.lock index 33f78958017..418ada3e87e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1036,6 +1036,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -2387,10 +2392,10 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": - version "7.23.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" - integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== +"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -2398,8 +2403,8 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.6" - "@babel/types" "^7.23.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" debug "^4.3.1" globals "^11.1.0" @@ -2455,6 +2460,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" From fc04f7ccb90935301d8bb2659fce706e207419c1 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 11 Feb 2024 22:12:29 +0100 Subject: [PATCH 15/49] position job correctly --- .github/workflows/pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ab78129cd17..47ee3df4091 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -131,7 +131,7 @@ jobs: fail_ci_if_error: true verbose: true -vitest: + vitest: name: Vitest Unit Tests runs-on: ubuntu-latest needs: [build] @@ -220,7 +220,7 @@ vitest: name: Canary runs-on: ubuntu-latest # ensure the basic checks pass before running the canary - needs: [build, jest, eslint, vitest, e2e] + needs: [build, jest-unit, jest-spec, eslint, vitest, e2e] if: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@v3 From 671df2f7af99298cfeed92afd2fc2b1369590f21 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Mon, 12 Feb 2024 08:13:23 +0100 Subject: [PATCH 16/49] avoid changed schema for now --- packages/graphiql/test/beforeDevServer.js | 9 +- packages/graphiql/test/schema.js | 240 ++++++++---------- .../src/__tests__/MessageProcessor.spec.ts | 1 - .../__tests__/__utils__/runSchemaServer.ts | 1 + 4 files changed, 108 insertions(+), 143 deletions(-) diff --git a/packages/graphiql/test/beforeDevServer.js b/packages/graphiql/test/beforeDevServer.js index 77e868c4057..d386ae47922 100644 --- a/packages/graphiql/test/beforeDevServer.js +++ b/packages/graphiql/test/beforeDevServer.js @@ -9,20 +9,13 @@ const express = require('express'); const path = require('node:path'); const { createHandler } = require('graphql-http/lib/use/express'); const schema = require('./schema'); -const { schema: badSchema, changedSchema } = require('./bad-schema'); +const { schema: badSchema } = require('./bad-schema'); module.exports = function beforeDevServer(app, _server, _compiler) { // GraphQL Server app.post('/graphql', createHandler({ schema })); app.get('/graphql', createHandler({ schema })); - app.post('/changed/graphql', createHandler({ schema: changedSchema })); - - app.post('/bad/graphql', (_req, res, next) => { - res.json({ data: badSchema }); - next(); - }); - app.post('/bad/graphql', (_req, res, next) => { res.json({ data: badSchema }); next(); diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js index 61612b13c16..fcd648096f1 100644 --- a/packages/graphiql/test/schema.js +++ b/packages/graphiql/test/schema.js @@ -230,115 +230,119 @@ And we have a cool logo: ![](/images/logo.svg) `.trim(); -const defaultFields = { - test: { - type: TestType, - description: '`test` field from `Test` type.', - resolve: () => ({}), - }, - deferrable: { - type: DeferrableObject, - resolve: () => ({}), - }, - streamable: { - type: new GraphQLList(Greeting), - args: { - delay: delayArgument(300), +const TestType = new GraphQLObjectType({ + name: 'Test', + description: 'Test type for testing\n New line works', + fields: () => ({ + test: { + type: TestType, + description: '`test` field from `Test` type.', + resolve: () => ({}), }, - resolve: async function* sayHiInSomeLanguages(_value, args) { - let i = 0; - for (const hi of [ - 'Hi', - '你好', - 'Hola', - 'أهلاً', - 'Bonjour', - 'سلام', - '안녕', - 'Ciao', - 'हेलो', - 'Здорово', - ]) { - if (i > 2) { - await sleep(args.delay); + deferrable: { + type: DeferrableObject, + resolve: () => ({}), + }, + streamable: { + type: new GraphQLList(Greeting), + args: { + delay: delayArgument(300), + }, + resolve: async function* sayHiInSomeLanguages(_value, args) { + let i = 0; + for (const hi of [ + 'Hi', + '你好', + 'Hola', + 'أهلاً', + 'Bonjour', + 'سلام', + '안녕', + 'Ciao', + 'हेलो', + 'Здорово', + ]) { + if (i > 2) { + await sleep(args.delay); + } + i++; + yield { text: hi }; } - i++; - yield { text: hi }; - } + }, }, - }, - person: { - type: Person, - resolve: () => ({ name: 'Mark' }), - }, - longDescriptionType: { - type: TestType, - description: longDescription, - resolve: () => ({}), - }, - union: { - type: TestUnion, - resolve: () => ({}), - }, - id: { - type: GraphQLID, - description: 'id field from Test type.', - resolve: () => 'abc123', - }, - isTest: { - type: GraphQLBoolean, - description: 'Is this a test schema? Sure it is.', - resolve: () => true, - }, - image: { - type: GraphQLString, - description: 'field that returns an image URI.', - resolve: () => '/images/logo.svg', - }, - deprecatedField: { - type: TestType, - description: 'This field is an example of a deprecated field', - deprecationReason: 'No longer in use, try `test` instead.', - }, - alsoDeprecated: { - type: TestType, - description: - 'This field is an example of a deprecated field with markdown in its deprecation reason', - deprecationReason: longDescription, - }, - hasArgs: { - type: GraphQLString, - resolve(_value, args) { - return JSON.stringify(args); + person: { + type: Person, + resolve: () => ({ name: 'Mark' }), + }, + longDescriptionType: { + type: TestType, + description: longDescription, + resolve: () => ({}), + }, + union: { + type: TestUnion, + resolve: () => ({}), }, - args: { - string: { type: GraphQLString, description: 'A string' }, - int: { type: GraphQLInt }, - float: { type: GraphQLFloat }, - boolean: { type: GraphQLBoolean }, - id: { type: GraphQLID }, - enum: { type: TestEnum }, - object: { type: TestInputObject }, - defaultValue: { - type: GraphQLString, - defaultValue: 'test default value', + id: { + type: GraphQLID, + description: 'id field from Test type.', + resolve: () => 'abc123', + }, + isTest: { + type: GraphQLBoolean, + description: 'Is this a test schema? Sure it is.', + resolve: () => true, + }, + image: { + type: GraphQLString, + description: 'field that returns an image URI.', + resolve: () => '/images/logo.svg', + }, + deprecatedField: { + type: TestType, + description: 'This field is an example of a deprecated field', + deprecationReason: 'No longer in use, try `test` instead.', + }, + alsoDeprecated: { + type: TestType, + description: + 'This field is an example of a deprecated field with markdown in its deprecation reason', + deprecationReason: longDescription, + }, + hasArgs: { + type: GraphQLString, + resolve(_value, args) { + return JSON.stringify(args); }, - // List - listString: { type: new GraphQLList(GraphQLString) }, - listInt: { type: new GraphQLList(GraphQLInt) }, - listFloat: { type: new GraphQLList(GraphQLFloat) }, - listBoolean: { type: new GraphQLList(GraphQLBoolean) }, - listID: { type: new GraphQLList(GraphQLID) }, - listEnum: { type: new GraphQLList(TestEnum) }, - listObject: { type: new GraphQLList(TestInputObject) }, - deprecatedArg: { - type: GraphQLString, - deprecationReason: 'deprecated argument', - description: 'Hello!', + args: { + string: { type: GraphQLString, description: 'A string' }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + defaultValue: { + type: GraphQLString, + defaultValue: 'test default value', + }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, + deprecatedArg: { + type: GraphQLString, + deprecationReason: 'deprecated argument', + description: 'Hello!', + }, }, }, - }, -}; + }), +}); const TestMutationType = new GraphQLObjectType({ name: 'MutationType', @@ -377,16 +381,6 @@ const TestSubscriptionType = new GraphQLObjectType({ }, }); -const getTestType = (fields = defaultFields) => { - return new GraphQLObjectType({ - name: 'Test', - description: 'Test type for testing\n New line works', - fields: () => fields, - }); -}; - -const TestType = getTestType(); - const myTestSchema = new GraphQLSchema({ query: TestType, mutation: TestMutationType, @@ -394,26 +388,4 @@ const myTestSchema = new GraphQLSchema({ description: 'This is a test schema for GraphiQL', }); -const ChangedTestType = getTestType({ - ...defaultFields, - newField: { - type: TestType, - resolve: () => ({}), - }, - isTest: { - type: GraphQLString, - description: 'Is this a test schema? Sure it is.', - resolve: () => true, - }, -}); - -const myChangedTestSchema = new GraphQLSchema({ - query: ChangedTestType, - mutation: TestMutationType, - subscription: TestSubscriptionType, - description: 'This is a changed test schema for GraphiQL', -}); - module.exports = myTestSchema; -module.exports.changedSchema = myChangedTestSchema; -module.exports.defaultFields = defaultFields; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 2998ca970cf..bf93515e16f 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -73,7 +73,6 @@ describe('MessageProcessor with no config', () => { }); expect(project.lsp._isInitialized).toEqual(true); expect(project.lsp._isGraphQLConfigMissing).toEqual(false); - expect(project.lsp._graphQLCache).toBeDefined(); }); }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts index e69de29bb2d..8b137891791 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts @@ -0,0 +1 @@ + From 432e5b98a10e57cb9301721e4949cc48ffee4a0c Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Mon, 12 Feb 2024 08:54:26 +0100 Subject: [PATCH 17/49] expose a slightly changed dev server --- packages/graphiql/test/beforeDevServer.js | 7 ++++++- packages/graphiql/test/e2e-server.js | 7 ++++++- packages/graphiql/test/schema.js | 21 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/graphiql/test/beforeDevServer.js b/packages/graphiql/test/beforeDevServer.js index d386ae47922..2cb0f4bfdc2 100644 --- a/packages/graphiql/test/beforeDevServer.js +++ b/packages/graphiql/test/beforeDevServer.js @@ -8,7 +8,7 @@ const express = require('express'); const path = require('node:path'); const { createHandler } = require('graphql-http/lib/use/express'); -const schema = require('./schema'); +const { schema, changedSchema } = require('./schema'); const { schema: badSchema } = require('./bad-schema'); module.exports = function beforeDevServer(app, _server, _compiler) { @@ -21,6 +21,11 @@ module.exports = function beforeDevServer(app, _server, _compiler) { next(); }); + app.post('/changed/graphql', (_req, res, next) => { + res.json({ data: changedSchema }); + next(); + }); + app.use('/images', express.static(path.join(__dirname, 'images'))); app.use( diff --git a/packages/graphiql/test/e2e-server.js b/packages/graphiql/test/e2e-server.js index a714e5be590..699e72ed346 100644 --- a/packages/graphiql/test/e2e-server.js +++ b/packages/graphiql/test/e2e-server.js @@ -10,7 +10,7 @@ const express = require('express'); const path = require('node:path'); const { createHandler } = require('graphql-http/lib/use/express'); const { GraphQLError } = require('graphql'); -const schema = require('./schema'); +const { schema, changedSchema } = require('./schema'); const app = express(); const { schema: badSchema } = require('./bad-schema'); const WebSocketsServer = require('./afterDevServer'); @@ -30,6 +30,11 @@ app.post('/bad/graphql', (_req, res, next) => { next(); }); +app.post('/changed/graphql', (_req, res, next) => { + res.json({ data: changedSchema }); + next(); +}); + app.post('/http-error/graphql', (_req, res, next) => { res.status(502).send('Bad Gateway'); next(); diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js index fcd648096f1..c630937ede4 100644 --- a/packages/graphiql/test/schema.js +++ b/packages/graphiql/test/schema.js @@ -73,6 +73,8 @@ const TestInputObject = new GraphQLInputObjectType({ }), }); +module.exports.testInputObject = TestInputObject; + const TestInterface = new GraphQLInterfaceType({ name: 'TestInterface', description: 'Test interface.', @@ -344,6 +346,23 @@ const TestType = new GraphQLObjectType({ }), }); +module.exports.testType = TestType; + +const ChangedSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + ...TestType.fields, + isTest: { + type: GraphQLString, + description: 'This is changed to a string field', + }, + }, + }), +}); + +module.exports.changedSchema = ChangedSchema; + const TestMutationType = new GraphQLObjectType({ name: 'MutationType', description: 'This is a simple mutation type', @@ -388,4 +407,4 @@ const myTestSchema = new GraphQLSchema({ description: 'This is a test schema for GraphiQL', }); -module.exports = myTestSchema; +module.exports.schema = myTestSchema; From f74a280bccce692c5a26adc00bc5e6e081b0ab2b Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Mon, 12 Feb 2024 13:08:03 +0100 Subject: [PATCH 18/49] tests not running seperately --- .github/workflows/pr.yml | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 47ee3df4091..3b5dda66eee 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -86,8 +86,8 @@ jobs: key: modules-${{ github.sha }} - run: yarn pretty-check - jest-unit: - name: Jest Unit Tests + jest: + name: Jest Unit & Integration Tests runs-on: ubuntu-latest needs: [install] steps: @@ -101,29 +101,7 @@ jobs: path: | **/node_modules key: modules-${{ github.sha }} - - run: yarn test:unit --coverage - - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage/lcov.info - fail_ci_if_error: true - verbose: true - jest-spec: - name: Jest Integration Tests - runs-on: ubuntu-latest - needs: [install] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 - - id: cache-modules - uses: actions/cache@v3 - with: - path: | - **/node_modules - key: modules-${{ github.sha }} - - run: yarn test:spec --coverage + - run: yarn test --coverage - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} From b7f905c816fc2c459c199199acc128cb79032168 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 16 Feb 2024 09:02:46 +0100 Subject: [PATCH 19/49] fix workflow deps --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3b5dda66eee..68ae3535a50 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -198,7 +198,7 @@ jobs: name: Canary runs-on: ubuntu-latest # ensure the basic checks pass before running the canary - needs: [build, jest-unit, jest-spec, eslint, vitest, e2e] + needs: [build, jest, eslint, vitest, e2e] if: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@v3 From b5bd0a428b95265e8a2158236a169e5c777b1733 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 16 Feb 2024 09:09:44 +0100 Subject: [PATCH 20/49] fix eslint --- packages/graphql-language-service-server/src/GraphQLCache.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 83ae8f3c94c..f3810f1e02a 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -485,7 +485,6 @@ export class GraphQLCache implements GraphQLCacheInterface { query, }; } catch { - console.log('parse error'); return { ast: null, query }; } }); From 935cb6f98d7e3c7d09ef4b1f029c36d164402973 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 16 Feb 2024 23:19:08 +0100 Subject: [PATCH 21/49] attempt to fix CI only test bug --- .vscode/settings.json | 2 +- .../src/__tests__/MessageProcessor.test.ts | 2 +- .../src/__tests__/__utils__/runSchemaServer.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e9b650d832b..411caa20536 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "files.insertFinalNewline": true, "editor.trimAutoWhitespace": false, "coverage-gutters.showLineCoverage": true, - "coverage-gutters.coverageBaseDir": "coverage", + "coverage-gutters.coverageBaseDir": "coverage/jest", "coverage-gutters.coverageFileNames": [ "lcov.info", "cov.xml", 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 4f9a18dca9d..9fd3cfab18b 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -338,7 +338,7 @@ describe('MessageProcessor', () => { const previousConfigurationValue = getConfigurationReturnValue; getConfigurationReturnValue = null; await expect( - messageProcessor.handleDidChangeConfiguration(), + messageProcessor.handleDidChangeConfiguration({ settings: [] }), ).resolves.toStrictEqual({}); getConfigurationReturnValue = previousConfigurationValue; }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts deleted file mode 100644 index 8b137891791..00000000000 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/runSchemaServer.ts +++ /dev/null @@ -1 +0,0 @@ - From 36242cddab91a3425f63448d29d4635f69a09068 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 16 Feb 2024 23:25:03 +0100 Subject: [PATCH 22/49] codecov config --- .github/workflows/main-test.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-test.yml b/.github/workflows/main-test.yml index 7a93be632ca..df186f35c2d 100644 --- a/.github/workflows/main-test.yml +++ b/.github/workflows/main-test.yml @@ -48,6 +48,6 @@ jobs: - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage/lcov.info + files: coverage/jest/lcov.info fail_ci_if_error: true verbose: true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 68ae3535a50..b3f3b4842bd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -105,7 +105,7 @@ jobs: - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage/lcov.info + files: coverage/jest/lcov.info fail_ci_if_error: true verbose: true From 8ab3c70ddc02f205bf5917452863a471e44b333f Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 16 Feb 2024 23:33:19 +0100 Subject: [PATCH 23/49] revert test schema change --- packages/graphiql/test/beforeDevServer.js | 7 +------ packages/graphiql/test/e2e-server.js | 7 +------ packages/graphiql/test/schema.js | 21 +-------------------- 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/packages/graphiql/test/beforeDevServer.js b/packages/graphiql/test/beforeDevServer.js index 2cb0f4bfdc2..d386ae47922 100644 --- a/packages/graphiql/test/beforeDevServer.js +++ b/packages/graphiql/test/beforeDevServer.js @@ -8,7 +8,7 @@ const express = require('express'); const path = require('node:path'); const { createHandler } = require('graphql-http/lib/use/express'); -const { schema, changedSchema } = require('./schema'); +const schema = require('./schema'); const { schema: badSchema } = require('./bad-schema'); module.exports = function beforeDevServer(app, _server, _compiler) { @@ -21,11 +21,6 @@ module.exports = function beforeDevServer(app, _server, _compiler) { next(); }); - app.post('/changed/graphql', (_req, res, next) => { - res.json({ data: changedSchema }); - next(); - }); - app.use('/images', express.static(path.join(__dirname, 'images'))); app.use( diff --git a/packages/graphiql/test/e2e-server.js b/packages/graphiql/test/e2e-server.js index 699e72ed346..a714e5be590 100644 --- a/packages/graphiql/test/e2e-server.js +++ b/packages/graphiql/test/e2e-server.js @@ -10,7 +10,7 @@ const express = require('express'); const path = require('node:path'); const { createHandler } = require('graphql-http/lib/use/express'); const { GraphQLError } = require('graphql'); -const { schema, changedSchema } = require('./schema'); +const schema = require('./schema'); const app = express(); const { schema: badSchema } = require('./bad-schema'); const WebSocketsServer = require('./afterDevServer'); @@ -30,11 +30,6 @@ app.post('/bad/graphql', (_req, res, next) => { next(); }); -app.post('/changed/graphql', (_req, res, next) => { - res.json({ data: changedSchema }); - next(); -}); - app.post('/http-error/graphql', (_req, res, next) => { res.status(502).send('Bad Gateway'); next(); diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js index c630937ede4..fcd648096f1 100644 --- a/packages/graphiql/test/schema.js +++ b/packages/graphiql/test/schema.js @@ -73,8 +73,6 @@ const TestInputObject = new GraphQLInputObjectType({ }), }); -module.exports.testInputObject = TestInputObject; - const TestInterface = new GraphQLInterfaceType({ name: 'TestInterface', description: 'Test interface.', @@ -346,23 +344,6 @@ const TestType = new GraphQLObjectType({ }), }); -module.exports.testType = TestType; - -const ChangedSchema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - ...TestType.fields, - isTest: { - type: GraphQLString, - description: 'This is changed to a string field', - }, - }, - }), -}); - -module.exports.changedSchema = ChangedSchema; - const TestMutationType = new GraphQLObjectType({ name: 'MutationType', description: 'This is a simple mutation type', @@ -407,4 +388,4 @@ const myTestSchema = new GraphQLSchema({ description: 'This is a test schema for GraphiQL', }); -module.exports.schema = myTestSchema; +module.exports = myTestSchema; From 8913d802e4fe98460352d877da09182439848a9c Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 12:58:01 +0100 Subject: [PATCH 24/49] revert config change, restore coverage --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 5e22fa5dd70..d4666de285d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ module.exports = { - ...require('./jest.config.base.js')(__dirname), + // ...require('./jest.config.base.js')(__dirname), projects: ['/packages/*/jest.config.js'], }; From bb984208e551d093452ce8e4d8a250b6fcfb93a3 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 12:59:43 +0100 Subject: [PATCH 25/49] revert config change, restore coverage --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b3f3b4842bd..68ae3535a50 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -105,7 +105,7 @@ jobs: - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage/jest/lcov.info + files: coverage/lcov.info fail_ci_if_error: true verbose: true From 98efb74d88488db70f962ff7dea03c7c45441ad0 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 14:53:50 +0100 Subject: [PATCH 26/49] cleanup --- .github/workflows/main-test.yml | 2 +- .../src/MessageProcessor.ts | 9 +++++- yarn.lock | 28 +++---------------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main-test.yml b/.github/workflows/main-test.yml index df186f35c2d..7a93be632ca 100644 --- a/.github/workflows/main-test.yml +++ b/.github/workflows/main-test.yml @@ -48,6 +48,6 @@ jobs: - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage/jest/lcov.info + files: coverage/lcov.info fail_ci_if_error: true verbose: true diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 9851683891e..b336e5c4fc2 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -1016,7 +1016,14 @@ export class MessageProcessor { // only local schema lookups if all of the schema entries are local files that we can resolve const sdlOnly = unwrappedSchema.every(schemaEntry => allExtensions.some( - ext => !schemaEntry.startsWith('http') && schemaEntry.endsWith(ext), + // local schema file URIs for lookup don't start with http, and end with an extension that is not json but may + // be graphql, gql, ts, js, javascript, or even vue, svelte, etc. + // would be awesome to use tree sitter to expand our parser to other languages, and then we could support SDL literal + // definitions in other languages! + ext => + !schemaEntry.startsWith('http') && + schemaEntry.endsWith(ext) && + ext !== 'json', ), ); // if we are caching the config schema, and it isn't a .graphql file, cache it diff --git a/yarn.lock b/yarn.lock index 418ada3e87e..0c29f0c3297 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7553,30 +7553,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001328: - version "1.0.30001450" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz" - integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== - -caniuse-lite@^1.0.30001406: - version "1.0.30001507" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001507.tgz#fae53f6286e7564783eadea9b447819410a59534" - integrity sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A== - -caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449: - version "1.0.30001457" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz#6af34bb5d720074e2099432aa522c21555a18301" - integrity sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA== - -caniuse-lite@^1.0.30001517: - version "1.0.30001518" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz#b3ca93904cb4699c01218246c4d77a71dbe97150" - integrity sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA== - -caniuse-lite@^1.0.30001565: - version "1.0.30001574" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz#fb4f1359c77f6af942510493672e1ec7ec80230c" - integrity sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001328, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001565: + version "1.0.30001588" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz" + integrity sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ== capital-case@^1.0.4: version "1.0.4" From 1a992353c8faaf8ad34f737e42c6bfb2987d196f Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 17:06:16 +0100 Subject: [PATCH 27/49] migrate over the wire tests to use local schema instance --- .vscode/settings.json | 2 +- package.json | 3 +- packages/graphiql/test/afterDevServer.js | 1 + packages/graphiql/test/e2e-server.js | 8 +++- .../src/MessageProcessor.ts | 13 ++----- .../src/__tests__/MessageProcessor.spec.ts | 39 ++++++++++++------- .../src/__tests__/__utils__/runServer.js | 1 + 7 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 411caa20536..e9b650d832b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "files.insertFinalNewline": true, "editor.trimAutoWhitespace": false, "coverage-gutters.showLineCoverage": true, - "coverage-gutters.coverageBaseDir": "coverage/jest", + "coverage-gutters.coverageBaseDir": "coverage", "coverage-gutters.coverageFileNames": [ "lcov.info", "cov.xml", diff --git a/package.json b/package.json index dfceeb3d3a6..1ce0f4e78df 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "cypress-open": "yarn workspace graphiql cypress-open", "dev-graphiql": "yarn workspace graphiql dev", "e2e": "yarn run e2e:build && yarn workspace graphiql e2e", + "e2e:server": "yarn workspace graphiql e2e-server", "e2e:build": "WEBPACK_SERVE=1 yarn workspace graphiql build-bundles", "eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --max-warnings=0 --ignore-path .gitignore --cache .", "format": "yarn eslint --fix && yarn pretty", @@ -70,7 +71,7 @@ "repo:fix": "manypkg fix", "repo:resolve": "node scripts/set-resolution.js", "t": "yarn test", - "test": "yarn jest", + "test": "yarn e2e:server yarn jest", "test:ci": "yarn build && jest --coverage && yarn vitest", "test:coverage": "yarn jest --coverage", "test:watch": "yarn jest --watch", diff --git a/packages/graphiql/test/afterDevServer.js b/packages/graphiql/test/afterDevServer.js index d47ef13f274..cf055ee66be 100644 --- a/packages/graphiql/test/afterDevServer.js +++ b/packages/graphiql/test/afterDevServer.js @@ -10,4 +10,5 @@ module.exports = function afterDevServer(_app, _server, _compiler) { }); // eslint-disable-next-line react-hooks/rules-of-hooks useServer({ schema }, wsServer); + return wsServer; }; diff --git a/packages/graphiql/test/e2e-server.js b/packages/graphiql/test/e2e-server.js index a714e5be590..d3847bcad4e 100644 --- a/packages/graphiql/test/e2e-server.js +++ b/packages/graphiql/test/e2e-server.js @@ -43,7 +43,9 @@ app.post('/graphql-error/graphql', (_req, res, next) => { app.use(express.static(path.resolve(__dirname, '../'))); app.use('index.html', express.static(path.resolve(__dirname, '../dev.html'))); -app.listen(process.env.PORT || 0, function () { +// messy but it allows close +const server = require('node:http').createServer(app); +server.listen(process.env.PORT || 3100, function () { const { port } = this.address(); console.log(`Started on http://localhost:${port}/`); @@ -56,5 +58,7 @@ app.listen(process.env.PORT || 0, function () { process.exit(); }); }); +const wsServer = WebSocketsServer(); -WebSocketsServer(); +module.exports.server = server; +module.exports.wsServer = wsServer; diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index b336e5c4fc2..55d1079646d 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -1013,17 +1013,12 @@ export class MessageProcessor { ...DEFAULT_SUPPORTED_EXTENSIONS, ...DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, ]; - // only local schema lookups if all of the schema entries are local files that we can resolve + // only local schema lookups if all of the schema entries are local files const sdlOnly = unwrappedSchema.every(schemaEntry => allExtensions.some( - // local schema file URIs for lookup don't start with http, and end with an extension that is not json but may - // be graphql, gql, ts, js, javascript, or even vue, svelte, etc. - // would be awesome to use tree sitter to expand our parser to other languages, and then we could support SDL literal - // definitions in other languages! - ext => - !schemaEntry.startsWith('http') && - schemaEntry.endsWith(ext) && - ext !== 'json', + // local schema file URIs for lookup don't start with http, and end with an extension. + // though it isn't often used, technically schema config could include a remote .graphql file + ext => !schemaEntry.startsWith('http') && schemaEntry.endsWith(ext), ), ); // if we are caching the config schema, and it isn't a .graphql file, cache it diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index bf93515e16f..8fb9b9e37a0 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -78,9 +78,17 @@ describe('MessageProcessor with no config', () => { }); describe('project with simple config and graphql files', () => { + let app; afterEach(() => { mockfs.restore(); }); + beforeAll(async () => { + app = await import('../../../graphiql/test/e2e-server'); + }); + afterAll(() => { + app.server.close(); + app.wsServer.close(); + }); it('caches files and schema with .graphql file config, and the schema updates with watched file changes', async () => { const project = new MockProject({ files: [ @@ -94,7 +102,6 @@ describe('project with simple config and graphql files', () => { }); await project.init('query.graphql'); expect(project.lsp._logger.error).not.toHaveBeenCalled(); - // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); // TODO: for some reason the cache result formats the graphql query?? const docCache = project.lsp._textDocumentCache; @@ -208,31 +215,34 @@ describe('project with simple config and graphql files', () => { it('caches files and schema with a URL config', async () => { const project = new MockProject({ files: [ - ['query.graphql', 'query { bar }'], - ['fragments.graphql', 'fragment Ep on Episode {\n created \n}'], + ['query.graphql', 'query { test { isTest, ...T } }'], + ['fragments.graphql', 'fragment T on Test {\n isTest \n}'], [ 'graphql.config.json', - '{ "schema": "https://rickandmortyapi.com/graphql", "documents": "./**.graphql" }', + '{ "schema": "http://localhost:3100/graphql", "documents": "./**.graphql" }', ], ], }); - await project.init('query.graphql'); + const initParams = await project.init('query.graphql'); + expect(initParams.diagnostics).toEqual([]); + + expect(project.lsp._logger.error).not.toHaveBeenCalled(); const changeParams = await project.lsp.handleDidChangeNotification({ textDocument: { uri: project.uri('query.graphql'), version: 1 }, - contentChanges: [ - { text: 'query { episodes { results { ...Ep, nop } } }' }, - ], + contentChanges: [{ text: 'query { test { isTest, ...T or } }' }], }); expect(changeParams?.diagnostics[0].message).toEqual( - 'Cannot query field "nop" on type "Episode".', + 'Cannot query field "or" on type "Test".', ); - expect(project.lsp._logger.error).not.toHaveBeenCalled(); - // console.log(project.lsp._graphQLCache.getSchema('schema.graphql')); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); + + // schema file is present and contains schema const file = await readFile(join(genSchemaPath), { encoding: 'utf-8' }); expect(file.split('\n').length).toBeGreaterThan(10); + + // hover works const hover = await project.lsp.handleHoverRequest({ position: { character: 10, @@ -240,14 +250,13 @@ describe('project with simple config and graphql files', () => { }, textDocument: { uri: project.uri('query.graphql') }, }); - expect(project.lsp._textDocumentCache.size).toEqual(3); + expect(hover.contents).toContain('`test` field from `Test` type.'); - expect(hover.contents).toContain('Get the list of all episodes'); + // ensure that fragment definitions work const definitions = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('query.graphql') }, - position: { character: 33, line: 0 }, + position: { character: 26, line: 0 }, }); - // ensure that fragment definitions work expect(definitions[0].uri).toEqual(project.uri('fragments.graphql')); expect(serializeRange(definitions[0].range)).toEqual({ start: { diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js new file mode 100644 index 00000000000..0e328a55450 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js @@ -0,0 +1 @@ +exports.default = require('../../../../graphiql/test/e2e-server.js'); From 7611f2cd7646bd3f620fcbd78bae0f1411667795 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 18:50:46 +0100 Subject: [PATCH 28/49] test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ce0f4e78df..adbf2d395e6 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "repo:fix": "manypkg fix", "repo:resolve": "node scripts/set-resolution.js", "t": "yarn test", - "test": "yarn e2e:server yarn jest", + "test": "yarn jest", "test:ci": "yarn build && jest --coverage && yarn vitest", "test:coverage": "yarn jest --coverage", "test:watch": "yarn jest --watch", From c35a1567861e7dba089ad0d187d34be262b611ec Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 19:04:57 +0100 Subject: [PATCH 29/49] try to fix this test --- .../src/__tests__/MessageProcessor.test.ts | 2 +- 1 file changed, 1 insertion(+), 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 9fd3cfab18b..4f9a18dca9d 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -338,7 +338,7 @@ describe('MessageProcessor', () => { const previousConfigurationValue = getConfigurationReturnValue; getConfigurationReturnValue = null; await expect( - messageProcessor.handleDidChangeConfiguration({ settings: [] }), + messageProcessor.handleDidChangeConfiguration(), ).resolves.toStrictEqual({}); getConfigurationReturnValue = previousConfigurationValue; }); From 128ac4a4cc56c921c52aa299f9b92e0459ceed62 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 18 Feb 2024 21:55:51 +0100 Subject: [PATCH 30/49] fix a few more things related to type cacheing --- .../src/GraphQLCache.ts | 82 ++++++++++++------- .../src/MessageProcessor.ts | 24 ++++-- .../src/__tests__/MessageProcessor.spec.ts | 45 +++++++++- 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index f3810f1e02a..c21f5cbe61d 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -433,11 +433,11 @@ export class GraphQLCache implements GraphQLCacheInterface { }; async updateFragmentDefinition( - rootDir: Uri, + projectCacheKey: Uri, filePath: Uri, contents: Array, ): Promise { - const cache = this._fragmentDefinitionsCache.get(rootDir); + const cache = this._fragmentDefinitionsCache.get(projectCacheKey); const asts = contents.map(({ query }) => { try { return { @@ -455,29 +455,44 @@ export class GraphQLCache implements GraphQLCacheInterface { cache.delete(key); } } - for (const { ast, query } of asts) { - if (!ast) { - continue; - } - for (const definition of ast.definitions) { - if (definition.kind === Kind.FRAGMENT_DEFINITION) { - cache.set(definition.name.value, { - filePath, - content: query, - definition, - }); - } + this._setFragmentCache(asts, cache, filePath); + } else { + const newFragmentCache = this._setFragmentCache( + asts, + new Map(), + filePath, + ); + this._fragmentDefinitionsCache.set(projectCacheKey, newFragmentCache); + } + } + _setFragmentCache( + asts: { ast: DocumentNode | null; query: string }[], + fragmentCache: Map, + filePath: string | undefined, + ) { + for (const { ast, query } of asts) { + if (!ast) { + continue; + } + for (const definition of ast.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + fragmentCache.set(definition.name.value, { + filePath, + content: query, + definition, + }); } } } + return fragmentCache; } async updateObjectTypeDefinition( - rootDir: Uri, + projectCacheKey: Uri, filePath: Uri, contents: Array, ): Promise { - const cache = this._typeDefinitionsCache.get(rootDir); + const cache = this._typeDefinitionsCache.get(projectCacheKey); const asts = contents.map(({ query }) => { try { return { @@ -495,21 +510,32 @@ export class GraphQLCache implements GraphQLCacheInterface { cache.delete(key); } } - for (const { ast, query } of asts) { - if (!ast) { - continue; - } - for (const definition of ast.definitions) { - if (isTypeDefinitionNode(definition)) { - cache.set(definition.name.value, { - filePath, - content: query, - definition, - }); - } + this._setDefinitionCache(asts, cache, filePath); + } else { + const newTypeCache = this._setDefinitionCache(asts, new Map(), filePath); + this._typeDefinitionsCache.set(projectCacheKey, newTypeCache); + } + } + _setDefinitionCache( + asts: { ast: DocumentNode | null; query: string }[], + typeCache: Map, + filePath: string | undefined, + ) { + for (const { ast, query } of asts) { + if (!ast) { + continue; + } + for (const definition of ast.definitions) { + if (isTypeDefinitionNode(definition)) { + typeCache.set(definition.name.value, { + filePath, + content: query, + definition, + }); } } } + return typeCache; } _extendSchema( diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 55d1079646d..d03e1408b52 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -926,12 +926,18 @@ export class MessageProcessor { return Array.from(this._textDocumentCache); } - private async _cacheSchemaText(uri: string, text: string, version: number) { + private async _cacheSchemaText( + uri: string, + text: string, + version: number, + project?: GraphQLProjectConfig, + ) { try { const contents = this._parser(text, uri); + // console.log(uri, contents); if (contents.length > 0) { await this._invalidateCache({ version, uri }, uri, contents); - await this._updateObjectTypeDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents, project); } } catch (err) { this._logger.error(String(err)); @@ -1058,10 +1064,10 @@ export class MessageProcessor { schemaText = `# This is an automatically generated representation of your schema.\n# Any changes to this file will be overwritten and will not be\n# reflected in the resulting GraphQL schema\n\n${schemaText}`; const cachedSchemaDoc = this._getCachedDocument(uri); - + this._graphQLCache._schemaMap.set(project.name, schema); if (!cachedSchemaDoc) { await writeFile(fsPath, schemaText, 'utf8'); - await this._cacheSchemaText(uri, schemaText, 1); + await this._cacheSchemaText(uri, schemaText, 0, project); } // do we have a change in the getSchema result? if so, update schema cache if (cachedSchemaDoc) { @@ -1070,6 +1076,7 @@ export class MessageProcessor { uri, schemaText, cachedSchemaDoc.version++, + project, ); } } @@ -1206,11 +1213,12 @@ export class MessageProcessor { private async _updateObjectTypeDefinition( uri: Uri, contents: CachedContent[], + project?: GraphQLProjectConfig, ): Promise { - const project = await this._graphQLCache.getProjectForFile(uri); - if (project) { - const cacheKey = this._graphQLCache._cacheKeyForProject(project); - + const resolvedProject = + project ?? (await this._graphQLCache.getProjectForFile(uri)); + if (resolvedProject) { + const cacheKey = this._graphQLCache._cacheKeyForProject(resolvedProject); await this._graphQLCache.updateObjectTypeDefinition( cacheKey, uri, diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 8fb9b9e37a0..9c033bda656 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -6,6 +6,7 @@ import { FileChangeType } from 'vscode-languageserver'; import { serializeRange } from './__utils__/utils'; import { readFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; +import { URI } from 'vscode-uri'; const defaultFiles = [ ['query.graphql', 'query { bar ...B }'], @@ -133,8 +134,8 @@ describe('project with simple config and graphql files', () => { character: 0, }, end: { - line: 0, - character: 25, + line: 2, + character: 1, }, }); // change the file to make the fragment invalid @@ -268,5 +269,45 @@ describe('project with simple config and graphql files', () => { character: 1, }, }); + + // TODO: super weird, the type definition cache isn't built until _after_ the first definitions request (for that file?)... + // this may be a bug just on init, or perhaps every definitions request is outdated??? + // local schema file should be used for definitions + + const typeDefinitions = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 15, line: 0 }, + }); + + // TODO: these should return a type definition from the schema + // + expect(typeDefinitions[0].uri).toEqual(URI.parse(genSchemaPath).toString()); + + expect(serializeRange(typeDefinitions[0].range)).toEqual({ + start: { + line: 10, + character: 0, + }, + end: { + line: 98, + character: 1, + }, + }); + + const schemaDefs = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: URI.parse(genSchemaPath).toString() }, + position: { character: 20, line: 17 }, + }); + expect(schemaDefs[0].uri).toEqual(URI.parse(genSchemaPath).toString()); + expect(serializeRange(schemaDefs[0].range)).toEqual({ + start: { + line: 100, + character: 0, + }, + end: { + line: 108, + character: 1, + }, + }); }); }); From 1910049c1cbbf804ba5e1ae421f17c8bdb934943 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 20 Feb 2024 05:09:15 +0100 Subject: [PATCH 31/49] fix embedded fragment definition offset bug! --- jest.config.base.js | 2 +- jest.config.js | 1 - package.json | 3 - .../src/GraphQLCache.ts | 6 +- .../src/MessageProcessor.ts | 17 +++-- .../src/__tests__/MessageProcessor.spec.ts | 36 ++++++++-- .../src/__tests__/MessageProcessor.test.ts | 14 ++-- .../src/__tests__/__utils__/MockProject.ts | 70 +++++++++++++++++++ .../src/__tests__/__utils__/runServer.js | 1 - .../src/interface/getDefinition.ts | 1 - 10 files changed, 126 insertions(+), 25 deletions(-) delete mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js diff --git a/jest.config.base.js b/jest.config.base.js index 6a401259a83..15e87eda8f8 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -33,7 +33,7 @@ module.exports = (dir, env = 'jsdom') => { // because of the svelte compiler's export patterns i guess? 'svelte/compiler': `${__dirname}/node_modules/svelte/compiler.cjs`, }, - testMatch: ['**/*[-.](test|spec).[jt]s?(x)', '!**/cypress/**'], + testMatch: ['**/*[-.](spec|test).[jt]s?(x)', '!**/cypress/**'], testEnvironment: env, testPathIgnorePatterns: ['node_modules', 'dist', 'cypress'], collectCoverageFrom: ['**/src/**/*.{js,jsx,ts,tsx}'], diff --git a/jest.config.js b/jest.config.js index d4666de285d..3ef34f68be1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,3 @@ module.exports = { - // ...require('./jest.config.base.js')(__dirname), projects: ['/packages/*/jest.config.js'], }; diff --git a/package.json b/package.json index adbf2d395e6..21399374029 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "cypress-open": "yarn workspace graphiql cypress-open", "dev-graphiql": "yarn workspace graphiql dev", "e2e": "yarn run e2e:build && yarn workspace graphiql e2e", - "e2e:server": "yarn workspace graphiql e2e-server", "e2e:build": "WEBPACK_SERVE=1 yarn workspace graphiql build-bundles", "eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --max-warnings=0 --ignore-path .gitignore --cache .", "format": "yarn eslint --fix && yarn pretty", @@ -75,8 +74,6 @@ "test:ci": "yarn build && jest --coverage && yarn vitest", "test:coverage": "yarn jest --coverage", "test:watch": "yarn jest --watch", - "test:spec": "TEST_ENV=spec yarn jest --testPathIgnorePatterns test.ts", - "test:unit": "yarn jest --testPathIgnorePatterns spec.ts", "tsc": "tsc --build", "vitest": "yarn wsrun -p -m test", "wsrun:noexamples": "wsrun --exclude-missing --exclude example-monaco-graphql-react-vite --exclude example-monaco-graphql-nextjs --exclude example-monaco-graphql-webpack --exclude example-graphiql-webpack" diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index c21f5cbe61d..9de2e35d165 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -646,9 +646,9 @@ export class GraphQLCache implements GraphQLCacheInterface { schema = this._extendSchema(schema, schemaPath, schemaCacheKey); } - // if (schemaCacheKey) { - // this._schemaMap.set(schemaCacheKey, schema); - // } + if (schemaCacheKey) { + this._schemaMap.set(schemaCacheKey, schema); + } return schema; }; diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index d03e1408b52..254d4399d58 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -672,8 +672,10 @@ export class MessageProcessor { const text = await readFile(URI.parse(uri).fsPath, 'utf-8'); const contents = this._parser(text, uri); + const cachedDocument = this._textDocumentCache.get(uri); + const version = cachedDocument ? cachedDocument.version++ : 0; await this._invalidateCache( - { uri, version: 0 }, + { uri, version }, URI.parse(uri).fsPath, contents, ); @@ -796,10 +798,17 @@ export class MessageProcessor { if (parentRange && res.name) { const isInline = inlineFragments.includes(res.name); const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes( - path.extname(textDocument.uri) as SupportedExtensionsEnum, + path.extname(res.path) as SupportedExtensionsEnum, ); - if (isInline && isEmbedded) { - const vOffset = parentRange.start.line; + + if (isEmbedded || isInline) { + const cachedDoc = this._getCachedDocument( + URI.parse(res.path).toString(), + ); + const vOffset = isEmbedded + ? cachedDoc?.contents[0].range?.start.line ?? 0 + : parentRange.start.line; + defRange.setStart( (defRange.start.line += vOffset), defRange.start.character, diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 9c033bda656..2763f169dcb 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -220,7 +220,7 @@ describe('project with simple config and graphql files', () => { ['fragments.graphql', 'fragment T on Test {\n isTest \n}'], [ 'graphql.config.json', - '{ "schema": "http://localhost:3100/graphql", "documents": "./**.graphql" }', + '{ "schema": "http://localhost:3100/graphql", "documents": "./**" }', ], ], }); @@ -270,17 +270,11 @@ describe('project with simple config and graphql files', () => { }, }); - // TODO: super weird, the type definition cache isn't built until _after_ the first definitions request (for that file?)... - // this may be a bug just on init, or perhaps every definitions request is outdated??? - // local schema file should be used for definitions - const typeDefinitions = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('fragments.graphql') }, position: { character: 15, line: 0 }, }); - // TODO: these should return a type definition from the schema - // expect(typeDefinitions[0].uri).toEqual(URI.parse(genSchemaPath).toString()); expect(serializeRange(typeDefinitions[0].range)).toEqual({ @@ -309,5 +303,33 @@ describe('project with simple config and graphql files', () => { character: 1, }, }); + await project.deleteFile('fragments.graphql'); + await project.addFile( + 'fragments.ts', + '\n\nexport const fragment = \ngql`\n\n fragment T on Test { isTest }\n`', + ); + + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('fragments.ts'), type: FileChangeType.Created }, + ], + }); + const defsForTs = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, + position: { character: 26, line: 0 }, + }); + + expect(defsForTs[0].uri).toEqual(project.uri('fragments.ts')); + expect(serializeRange(defsForTs[0].range)).toEqual({ + start: { + line: 5, + character: 2, + }, + end: { + // TODO! line is wrong, it expects 1 for some reason probably in the LanguageService here + line: 5, + character: 31, + }, + }); }); }); 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 4f9a18dca9d..c84f7d0f024 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -289,7 +289,7 @@ describe('MessageProcessor', () => { it('runs document symbol requests when not initialized', async () => { const test = { textDocument: { - uri: `${queryPathUri}/test5.graphql`, + uri: `${queryPathUri}/test3.graphql`, version: 0, }, }; @@ -297,6 +297,10 @@ describe('MessageProcessor', () => { const result = await messageProcessor.handleDocumentSymbolRequest(test); expect(result).toEqual([]); messageProcessor._isInitialized = true; + const nextResult = await messageProcessor.handleDocumentSymbolRequest(test); + expect(nextResult[0].location.uri).toContain('test3.graphql'); + expect(nextResult[0].name).toEqual('item'); + expect(nextResult.length).toEqual(1); }); it('properly changes the file cache with the didChange handler', async () => { @@ -335,11 +339,13 @@ describe('MessageProcessor', () => { }); it('does not crash on null value returned in response to workspace configuration', async () => { + // for some reason this is needed? can't be a good thing... must have done something to cause a performance hit on + // loading config schema.. + jest.setTimeout(10000); const previousConfigurationValue = getConfigurationReturnValue; getConfigurationReturnValue = null; - await expect( - messageProcessor.handleDidChangeConfiguration(), - ).resolves.toStrictEqual({}); + const result = await messageProcessor.handleDidChangeConfiguration(); + expect(result).toEqual({}); getConfigurationReturnValue = previousConfigurationValue; }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index f1511c11df6..a71e2031970 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -2,6 +2,8 @@ import mockfs from 'mock-fs'; import { MessageProcessor } from '../../MessageProcessor'; import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import { URI } from 'vscode-uri'; +import { FileChangeType } from 'vscode-languageserver'; +import { FileChangeTypeKind } from 'graphql-language-service'; export type MockFile = [filename: string, text: string]; @@ -104,6 +106,74 @@ export class MockProject { this.fileCache.set(filename, text); this.mockFiles(); } + async addFile(filename: string, text: string) { + this.fileCache.set(filename, text); + this.mockFiles(); + await this.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeTypeKind.Created, + text, + }, + ], + textDocument: { + uri: this.uri(filename), + version: 2, + }, + }); + } + async changeWatchedFile(filename: string, text: string) { + this.changeFile(filename, text); + await this.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: this.uri(filename), + type: FileChangeType.Changed, + }, + ], + }); + } + async saveOpenFile(filename: string, text: string) { + this.changeFile(filename, text); + await this.lsp.handleDidOpenOrSaveNotification({ + textDocument: { + uri: this.uri(filename), + version: 2, + text, + }, + }); + } + async addWatchedFile(filename: string, text: string) { + this.changeFile(filename, text); + await this.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeTypeKind.Created, + text, + }, + ], + textDocument: { + uri: this.uri(filename), + version: 2, + }, + }); + } + async deleteFile(filename: string) { + this.fileCache.delete(filename); + this.mockFiles(); + await this.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeTypeKind.Deleted, + text: '', + }, + ], + textDocument: { + uri: this.uri(filename), + version: 2, + }, + }); + } get lsp() { return this.messageProcessor; } diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js deleted file mode 100644 index 0e328a55450..00000000000 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js +++ /dev/null @@ -1 +0,0 @@ -exports.default = require('../../../../graphiql/test/e2e-server.js'); diff --git a/packages/graphql-language-service/src/interface/getDefinition.ts b/packages/graphql-language-service/src/interface/getDefinition.ts index 952ca33db47..dea00033a19 100644 --- a/packages/graphql-language-service/src/interface/getDefinition.ts +++ b/packages/graphql-language-service/src/interface/getDefinition.ts @@ -124,7 +124,6 @@ export async function getDefinitionQueryResultForFragmentSpread( ({ filePath, content, definition }) => getDefinitionForFragmentDefinition(filePath || '', content, definition), ); - return { definitions, queryRange: definitions.map(_ => getRange(text, fragment)), From f025e7225bd1a96e610d01e8955127a405245b02 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 20 Feb 2024 05:12:48 +0100 Subject: [PATCH 32/49] spelling bug --- .../src/__tests__/MessageProcessor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 2763f169dcb..d793f57c663 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -306,7 +306,7 @@ describe('project with simple config and graphql files', () => { await project.deleteFile('fragments.graphql'); await project.addFile( 'fragments.ts', - '\n\nexport const fragment = \ngql`\n\n fragment T on Test { isTest }\n`', + '\n\n\nexport const fragment = gql`\n\n fragment T on Test { isTest }\n`', ); await project.lsp.handleWatchedFilesChangedNotification({ From 0395e646bee9db5d75e8183984a3e40cc17cfe7e Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Wed, 21 Feb 2024 00:24:09 +0100 Subject: [PATCH 33/49] cleanup --- .../src/__tests__/MessageProcessor.spec.ts | 5 +++-- .../src/__tests__/__utils__/runServer.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index d793f57c663..27921298dcb 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -303,7 +303,9 @@ describe('project with simple config and graphql files', () => { character: 1, }, }); + // lets remove the fragments file await project.deleteFile('fragments.graphql'); + // and add a fragments.ts file await project.addFile( 'fragments.ts', '\n\n\nexport const fragment = gql`\n\n fragment T on Test { isTest }\n`', @@ -318,7 +320,7 @@ describe('project with simple config and graphql files', () => { textDocument: { uri: project.uri('query.graphql') }, position: { character: 26, line: 0 }, }); - + // this one is really important expect(defsForTs[0].uri).toEqual(project.uri('fragments.ts')); expect(serializeRange(defsForTs[0].range)).toEqual({ start: { @@ -326,7 +328,6 @@ describe('project with simple config and graphql files', () => { character: 2, }, end: { - // TODO! line is wrong, it expects 1 for some reason probably in the LanguageService here line: 5, character: 31, }, diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js new file mode 100644 index 00000000000..0e328a55450 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js @@ -0,0 +1 @@ +exports.default = require('../../../../graphiql/test/e2e-server.js'); From ba7fd81f4723ef5543392002d98bb06cd4a14cf0 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 27 Feb 2024 19:52:06 +0100 Subject: [PATCH 34/49] fix: cleanup, potentially fix project name cache key bug? --- .../src/GraphQLCache.ts | 6 +- .../src/MessageProcessor.ts | 9 +- .../src/__tests__/MessageProcessor.spec.ts | 139 +++++++++++++++++- .../src/__tests__/MessageProcessor.test.ts | 12 +- .../src/__tests__/__utils__/MockProject.ts | 28 ++-- .../graphql-language-service/src/types.ts | 7 +- 6 files changed, 170 insertions(+), 31 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 9de2e35d165..e76fa102a3d 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -601,10 +601,10 @@ export class GraphQLCache implements GraphQLCacheInterface { } getSchema = async ( - appName?: string, + projectName: string, queryHasExtensions?: boolean | null, ): Promise => { - const projectConfig = this._graphQLConfig.getProject(appName); + const projectConfig = this._graphQLConfig.getProject(projectName); if (!projectConfig) { return null; @@ -668,7 +668,7 @@ export class GraphQLCache implements GraphQLCacheInterface { } _getProjectName(projectConfig: GraphQLProjectConfig) { - return projectConfig || 'default'; + return projectConfig?.name || 'default'; } /** diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 254d4399d58..cd484b549b5 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -32,7 +32,6 @@ import type { DidOpenTextDocumentParams, DidChangeConfigurationParams, Diagnostic, - CompletionItem, CompletionList, CancellationToken, Hover, @@ -544,9 +543,9 @@ export class MessageProcessor { async handleCompletionRequest( params: CompletionParams, - ): Promise> { + ): Promise { if (!this._isInitialized) { - return []; + return { items: [], isIncomplete: false }; } this.validateDocumentAndPosition(params); @@ -560,7 +559,7 @@ export class MessageProcessor { const cachedDocument = this._getCachedDocument(textDocument.uri); if (!cachedDocument) { - return []; + return { items: [], isIncomplete: false }; } const found = cachedDocument.contents.find(content => { @@ -572,7 +571,7 @@ export class MessageProcessor { // If there is no GraphQL query in this file, return an empty result. if (!found) { - return []; + return { items: [], isIncomplete: false }; } const { query, range } = found; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 27921298dcb..79a6f013247 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -14,7 +14,7 @@ const defaultFiles = [ ] as MockFile[]; const schemaFile: MockFile = [ 'schema.graphql', - 'type Query { foo: Foo }\n\ntype Foo { bar: String }', + 'type Query { foo: Foo }\n\ntype Foo { bar: String }', ]; const genSchemaPath = @@ -102,6 +102,22 @@ describe('project with simple config and graphql files', () => { ], }); await project.init('query.graphql'); + const initSchemaDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('schema.graphql') }, + position: { character: 19, line: 0 }, + }); + expect(initSchemaDefRequest.length).toEqual(1); + expect(initSchemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(initSchemaDefRequest[0].range)).toEqual({ + start: { + line: 2, + character: 0, + }, + end: { + character: 24, + line: 2, + }, + }); expect(project.lsp._logger.error).not.toHaveBeenCalled(); expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); // TODO: for some reason the cache result formats the graphql query?? @@ -151,8 +167,26 @@ describe('project with simple config and graphql files', () => { }); const typeCache = project.lsp._graphQLCache._typeDefinitionsCache.get('/tmp/test-default'); - expect(typeCache?.get('Test')?.definition.name.value).toEqual('Test'); + + // test in-file schema defs! important! + const schemaDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('schema.graphql') }, + position: { character: 19, line: 0 }, + }); + expect(schemaDefRequest.length).toEqual(1); + expect(schemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(schemaDefRequest[0].range)).toEqual({ + start: { + line: 7, + character: 0, + }, + end: { + character: 21, + line: 7, + }, + }); + // TODO: this fragment should now be invalid const result = await project.lsp.handleDidOpenOrSaveNotification({ textDocument: { uri: project.uri('fragments.graphql') }, @@ -237,7 +271,7 @@ describe('project with simple config and graphql files', () => { expect(changeParams?.diagnostics[0].message).toEqual( 'Cannot query field "or" on type "Test".', ); - expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); + expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); // schema file is present and contains schema const file = await readFile(join(genSchemaPath), { encoding: 'utf-8' }); @@ -305,10 +339,11 @@ describe('project with simple config and graphql files', () => { }); // lets remove the fragments file await project.deleteFile('fragments.graphql'); - // and add a fragments.ts file + // and add a fragments.ts file, watched await project.addFile( 'fragments.ts', '\n\n\nexport const fragment = gql`\n\n fragment T on Test { isTest }\n`', + true, ); await project.lsp.handleWatchedFilesChangedNotification({ @@ -333,4 +368,100 @@ describe('project with simple config and graphql files', () => { }, }); }); + it('caches multiple projects with files and schema with a URL config and a local schema', async () => { + const project = new MockProject({ + files: [ + [ + 'a/fragments.ts', + '\n\n\nexport const fragment = gql`\n\n fragment TestFragment on Test { isTest }\n`', + ], + [ + 'a/query.ts', + '\n\n\nexport const query = gql`query { test() { isTest, ...T } }`', + ], + + [ + 'b/query.ts', + 'import graphql from "graphql"\n\n\nconst a = graphql` query example { test() { isTest ...T } }`', + ], + [ + 'b/fragments.ts', + '\n\n\nexport const fragment = gql`\n\n fragment T on Test { isTest }\n`', + ], + ['b/schema.graphql', schemaFile[1]], + [ + 'package.json', + `{ "graphql": { "projects": { + "a": { "schema": "http://localhost:3100/graphql", "documents": "./a/**" }, + "b": { "schema": "./b/schema.graphql", "documents": "./b/**" } } + } + }`, + ], + schemaFile, + ], + }); + + const initParams = await project.init('a/query.graphql'); + expect(initParams.diagnostics).toEqual([]); + + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + expect(await project.lsp._graphQLCache.getSchema('a')).toBeDefined(); + const file = await readFile(join(genSchemaPath.replace('default', 'a')), { + encoding: 'utf-8', + }); + expect(file.split('\n').length).toBeGreaterThan(10); + // add a new typescript file with empty query to the b project + // and expect autocomplete to only show options for project b + await project.addFile( + 'b/empty.ts', + 'import gql from "graphql-tag"\ngql`query a { }`', + ); + const completion = await project.lsp.handleCompletionRequest({ + textDocument: { uri: project.uri('b/empty.ts') }, + position: { character: 13, line: 1 }, + }); + + expect(completion.items?.length).toEqual(4); + expect(completion.items.map(i => i.label)).toEqual([ + 'foo', + '__typename', + '__schema', + '__type', + ]); + + // TODO this didn't work at all, how to register incomplete changes to model autocomplete, etc? + // project.changeFile( + // 'b/schema.graphql', + // schemaFile[1] + '\ntype Example1 { field: }', + // ); + // await project.lsp.handleWatchedFilesChangedNotification({ + // changes: [ + // { uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed }, + // ], + // }); + // better - fails on a graphql parsing error! annoying + // await project.lsp.handleDidChangeNotification({ + // textDocument: { uri: project.uri('b/schema.graphql'), version: 1 }, + // contentChanges: [ + // { text: schemaFile[1] + '\ntype Example1 { field: }' }, + // ], + // }); + + // const schemaCompletion = await project.lsp.handleCompletionRequest({ + // textDocument: { uri: project.uri('b/schema.graphql') }, + // position: { character: 23, line: 3 }, + // }); + // expect(schemaCompletion.items.map(i => i.label)).toEqual([ + // 'foo', + // '__typename', + // '__schema', + // '__type', + // ]); + // this confirms that autocomplete respects cross-project boundaries for types + const schemaCompletion = await project.lsp.handleCompletionRequest({ + textDocument: { uri: project.uri('b/schema.graphql') }, + position: { character: 21, line: 0 }, + }); + expect(schemaCompletion.items.map(i => i.label)).toEqual(['Foo']); + }); }); 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 c84f7d0f024..ce36f32c4bd 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -219,7 +219,10 @@ describe('MessageProcessor', () => { textDocument: { uri: `${queryPathUri}/test13.graphql` }, }; const result = await messageProcessor.handleCompletionRequest(test); - expect(result).toEqual([]); + expect(result).toEqual({ + items: [], + isIncomplete: false, + }); }); it('runs completion requests properly when not initialized', async () => { const test = { @@ -228,7 +231,10 @@ describe('MessageProcessor', () => { }; messageProcessor._isInitialized = false; const result = await messageProcessor.handleCompletionRequest(test); - expect(result).toEqual([]); + expect(result).toEqual({ + items: [], + isIncomplete: false, + }); }); it('runs document symbol requests', async () => { @@ -344,7 +350,7 @@ describe('MessageProcessor', () => { jest.setTimeout(10000); const previousConfigurationValue = getConfigurationReturnValue; getConfigurationReturnValue = null; - const result = await messageProcessor.handleDidChangeConfiguration(); + const result = await messageProcessor.handleDidChangeConfiguration({}); expect(result).toEqual({}); getConfigurationReturnValue = previousConfigurationValue; }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index a71e2031970..999066290c6 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -4,6 +4,7 @@ import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import { URI } from 'vscode-uri'; import { FileChangeType } from 'vscode-languageserver'; import { FileChangeTypeKind } from 'graphql-language-service'; +import { mock } from 'fetch-mock'; export type MockFile = [filename: string, text: string]; @@ -83,7 +84,7 @@ export class MockProject { }); return this.lsp.handleDidOpenOrSaveNotification({ textDocument: { - uri: this.uri(filename || this.uri('query.graphql')), + uri: this.uri(filename || 'query.graphql'), version: 1, text: this.fileCache.get('query.graphql') || fileText, }, @@ -106,9 +107,19 @@ export class MockProject { this.fileCache.set(filename, text); this.mockFiles(); } - async addFile(filename: string, text: string) { + async addFile(filename: string, text: string, watched = false) { this.fileCache.set(filename, text); this.mockFiles(); + if (watched) { + await this.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: this.uri(filename), + type: FileChangeTypeKind.Created, + }, + ], + }); + } await this.lsp.handleDidChangeNotification({ contentChanges: [ { @@ -159,19 +170,16 @@ export class MockProject { }); } async deleteFile(filename: string) { + mockfs.restore(); this.fileCache.delete(filename); this.mockFiles(); - await this.lsp.handleDidChangeNotification({ - contentChanges: [ + await this.lsp.handleWatchedFilesChangedNotification({ + changes: [ { - type: FileChangeTypeKind.Deleted, - text: '', + type: FileChangeType.Deleted, + uri: this.uri(filename), }, ], - textDocument: { - uri: this.uri(filename), - version: 2, - }, }); } get lsp() { diff --git a/packages/graphql-language-service/src/types.ts b/packages/graphql-language-service/src/types.ts index 7ed008290f4..f0d4f906763 100644 --- a/packages/graphql-language-service/src/types.ts +++ b/packages/graphql-language-service/src/types.ts @@ -50,11 +50,6 @@ export interface GraphQLCache { getProjectForFile: (uri: string) => GraphQLProjectConfig | void; - getObjectTypeDependencies: ( - query: string, - fragmentDefinitions: Map, - ) => Promise; - getObjectTypeDependenciesForAST: ( parsedQuery: ASTNode, fragmentDefinitions: Map, @@ -90,7 +85,7 @@ export interface GraphQLCache { contents: CachedContent[], ) => Promise; getSchema: ( - appName?: string, + appName: string, queryHasExtensions?: boolean, ) => Promise; } From 0b7115b0078c77c2de9290dece74013f37d760e1 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 27 Feb 2024 21:20:01 +0100 Subject: [PATCH 35/49] fix: delete the unused method --- .../src/GraphQLCache.ts | 23 ------ .../src/__tests__/MessageProcessor.spec.ts | 80 +++++++++++-------- 2 files changed, 45 insertions(+), 58 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index e76fa102a3d..224a6cf2636 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -244,29 +244,6 @@ export class GraphQLCache implements GraphQLCacheInterface { return fragmentDefinitions; }; - getObjectTypeDependencies = async ( - query: string, - objectTypeDefinitions?: Map, - ): Promise> => { - // If there isn't context for object type references, - // return an empty array. - if (!objectTypeDefinitions) { - return []; - } - // If the query cannot be parsed, validations cannot happen yet. - // Return an empty array. - let parsedQuery; - try { - parsedQuery = parse(query); - } catch { - return []; - } - return this.getObjectTypeDependenciesForAST( - parsedQuery, - objectTypeDefinitions, - ); - }; - getObjectTypeDependenciesForAST = async ( parsedQuery: ASTNode, objectTypeDefinitions: Map, diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 79a6f013247..df83013aa15 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -14,12 +14,25 @@ const defaultFiles = [ ] as MockFile[]; const schemaFile: MockFile = [ 'schema.graphql', - 'type Query { foo: Foo }\n\ntype Foo { bar: String }', + 'type Query { foo: Foo, test: Test }\n\ntype Foo { bar: String }\n\ntype Test { test: Foo }', ]; +const fooTypePosition = { + start: { line: 2, character: 0 }, + end: { line: 2, character: 24 }, +}; + const genSchemaPath = '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql'; +// TODO: +// - reorganize into multiple files +// - potentially a high level abstraction and/or it.each() for a pathway across configs, file extensions, etc. +// this may be cumbersome with offset position assertions but possible +// if we can create consistency that doesn't limit variability +// - convert each it() into a nested describe() block (or a top level describe() in another file), and sprinkle in it() statements to replace comments +// - fix TODO comments where bugs were found that couldn't be resolved quickly (2-4hr timebox) + describe('MessageProcessor with no config', () => { afterEach(() => { mockfs.restore(); @@ -108,18 +121,11 @@ describe('project with simple config and graphql files', () => { }); expect(initSchemaDefRequest.length).toEqual(1); expect(initSchemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); - expect(serializeRange(initSchemaDefRequest[0].range)).toEqual({ - start: { - line: 2, - character: 0, - }, - end: { - character: 24, - line: 2, - }, - }); + expect(serializeRange(initSchemaDefRequest[0].range)).toEqual( + fooTypePosition, + ); expect(project.lsp._logger.error).not.toHaveBeenCalled(); - expect(await project.lsp._graphQLCache.getSchema()).toBeDefined(); + expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); // TODO: for some reason the cache result formats the graphql query?? const docCache = project.lsp._textDocumentCache; expect( @@ -131,10 +137,7 @@ describe('project with simple config and graphql files', () => { }); expect(schemaDefinitions[0].uri).toEqual(project.uri('schema.graphql')); - expect(serializeRange(schemaDefinitions[0].range).end).toEqual({ - line: 2, - character: 24, - }); + expect(serializeRange(schemaDefinitions[0].range)).toEqual(fooTypePosition); // query definition request of fragment name jumps to the fragment definition const firstQueryDefRequest = await project.lsp.handleDefinitionRequest({ @@ -174,18 +177,16 @@ describe('project with simple config and graphql files', () => { textDocument: { uri: project.uri('schema.graphql') }, position: { character: 19, line: 0 }, }); + + const fooLaterTypePosition = { + start: { line: 7, character: 0 }, + end: { line: 7, character: 21 }, + }; expect(schemaDefRequest.length).toEqual(1); expect(schemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); - expect(serializeRange(schemaDefRequest[0].range)).toEqual({ - start: { - line: 7, - character: 0, - }, - end: { - character: 21, - line: 7, - }, - }); + expect(serializeRange(schemaDefRequest[0].range)).toEqual( + fooLaterTypePosition, + ); // TODO: this fragment should now be invalid const result = await project.lsp.handleDidOpenOrSaveNotification({ @@ -241,10 +242,9 @@ describe('project with simple config and graphql files', () => { project.uri('schema.graphql'), ); - expect(serializeRange(schemaDefinitionsAgain[0].range).end).toEqual({ - line: 7, - character: 21, - }); + expect(serializeRange(schemaDefinitionsAgain[0].range)).toEqual( + fooLaterTypePosition, + ); // TODO: the position should change when a watched file changes??? }); it('caches files and schema with a URL config', async () => { @@ -327,6 +327,8 @@ describe('project with simple config and graphql files', () => { position: { character: 20, line: 17 }, }); expect(schemaDefs[0].uri).toEqual(URI.parse(genSchemaPath).toString()); + // note: if the graphiql test schema changes, + // this might break, please adjust if you see a failure here expect(serializeRange(schemaDefs[0].range)).toEqual({ start: { line: 100, @@ -421,19 +423,20 @@ describe('project with simple config and graphql files', () => { position: { character: 13, line: 1 }, }); - expect(completion.items?.length).toEqual(4); + expect(completion.items?.length).toEqual(5); expect(completion.items.map(i => i.label)).toEqual([ 'foo', + 'test', '__typename', '__schema', '__type', ]); - // TODO this didn't work at all, how to register incomplete changes to model autocomplete, etc? // project.changeFile( // 'b/schema.graphql', // schemaFile[1] + '\ntype Example1 { field: }', // ); + // TODO: this didn't work at all, how to register incomplete changes to model autocomplete, etc? // await project.lsp.handleWatchedFilesChangedNotification({ // changes: [ // { uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed }, @@ -457,11 +460,18 @@ describe('project with simple config and graphql files', () => { // '__schema', // '__type', // ]); - // this confirms that autocomplete respects cross-project boundaries for types - const schemaCompletion = await project.lsp.handleCompletionRequest({ + // this confirms that autocomplete respects cross-project boundaries for types. + // it performs a definition request for the foo field in Query + const schemaCompletion1 = await project.lsp.handleCompletionRequest({ textDocument: { uri: project.uri('b/schema.graphql') }, position: { character: 21, line: 0 }, }); - expect(schemaCompletion.items.map(i => i.label)).toEqual(['Foo']); + expect(schemaCompletion1.items.map(i => i.label)).toEqual(['Foo']); + // it performs a definition request for the Foo type in Test.test + const schemaDefinition = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('b/schema.graphql') }, + position: { character: 21, line: 4 }, + }); + expect(serializeRange(schemaDefinition[0].range)).toEqual(fooTypePosition); }); }); From 833a727760d23fdcd69bde95a467b67470fc8c74 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 27 Feb 2024 21:31:47 +0100 Subject: [PATCH 36/49] add comments --- .../src/__tests__/MessageProcessor.spec.ts | 9 ++++++--- .../src/__tests__/__utils__/MockProject.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index df83013aa15..27a426554ba 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -31,7 +31,7 @@ const genSchemaPath = // this may be cumbersome with offset position assertions but possible // if we can create consistency that doesn't limit variability // - convert each it() into a nested describe() block (or a top level describe() in another file), and sprinkle in it() statements to replace comments -// - fix TODO comments where bugs were found that couldn't be resolved quickly (2-4hr timebox) +// - fix TODO comments where bugs were found that couldn't be resolved quickly (2-4hr time box) describe('MessageProcessor with no config', () => { afterEach(() => { @@ -327,7 +327,7 @@ describe('project with simple config and graphql files', () => { position: { character: 20, line: 17 }, }); expect(schemaDefs[0].uri).toEqual(URI.parse(genSchemaPath).toString()); - // note: if the graphiql test schema changes, + // note: if the graphiql test schema changes, // this might break, please adjust if you see a failure here expect(serializeRange(schemaDefs[0].range)).toEqual({ start: { @@ -436,7 +436,10 @@ describe('project with simple config and graphql files', () => { // 'b/schema.graphql', // schemaFile[1] + '\ntype Example1 { field: }', // ); - // TODO: this didn't work at all, how to register incomplete changes to model autocomplete, etc? + // TODO: this didn't work at all for multi project, + // whereas a schema change works above in a single schema context as per updating the cache + // + // how to register incomplete changes to model autocomplete, etc? // await project.lsp.handleWatchedFilesChangedNotification({ // changes: [ // { uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed }, diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index 999066290c6..6cd215a9424 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -4,7 +4,6 @@ import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import { URI } from 'vscode-uri'; import { FileChangeType } from 'vscode-languageserver'; import { FileChangeTypeKind } from 'graphql-language-service'; -import { mock } from 'fetch-mock'; export type MockFile = [filename: string, text: string]; @@ -74,6 +73,7 @@ export class MockProject { loadConfigOptions: { rootDir: root }, }); } + public async init(filename?: string, fileText?: string) { await this.lsp.handleInitializeRequest({ rootPath: this.root, From 525c56978a07cc1ea4f79e58d25a1fcf3b582d4d Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 27 Feb 2024 23:00:39 +0100 Subject: [PATCH 37/49] cleanup --- .../graphql-language-service-server/src/MessageProcessor.ts | 5 ++--- .../src/__tests__/MessageProcessor.spec.ts | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index cd484b549b5..e5d47d08413 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -975,8 +975,8 @@ export class MessageProcessor { const schemaText = await readFile(uri, 'utf8'); await this._cacheSchemaText(schemaUri, schemaText, version); } - } catch { - // this._logger.error(String(err)); + } catch (err) { + this._logger.error(String(err)); } } private _getTmpProjectPath( @@ -1184,7 +1184,6 @@ export class MessageProcessor { }), ); } - private _unwrapProjectSchema(project: GraphQLProjectConfig): string[] { const projectSchema = project.schema; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 27a426554ba..3b6eb14b4ba 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -245,6 +245,8 @@ describe('project with simple config and graphql files', () => { expect(serializeRange(schemaDefinitionsAgain[0].range)).toEqual( fooLaterTypePosition, ); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + // TODO: the position should change when a watched file changes??? }); it('caches files and schema with a URL config', async () => { @@ -369,6 +371,7 @@ describe('project with simple config and graphql files', () => { character: 31, }, }); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); it('caches multiple projects with files and schema with a URL config and a local schema', async () => { const project = new MockProject({ @@ -476,5 +479,7 @@ describe('project with simple config and graphql files', () => { position: { character: 21, line: 4 }, }); expect(serializeRange(schemaDefinition[0].range)).toEqual(fooTypePosition); + + expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); }); From cf4536becc689156ac51dbf5f73fdfc3b1806758 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 2 Mar 2024 14:31:06 +0100 Subject: [PATCH 38/49] feat: lazy initialization on watched file changes --- .../src/MessageProcessor.ts | 131 +++++++++--------- .../src/__tests__/MessageProcessor.spec.ts | 18 +-- .../src/__tests__/MessageProcessor.test.ts | 59 +++++++- 3 files changed, 127 insertions(+), 81 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index e5d47d08413..a2ba7b969df 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -63,7 +63,6 @@ import { ConfigEmptyError, ConfigInvalidError, ConfigNotFoundError, - GraphQLExtensionDeclaration, LoaderNoResultError, ProjectNotFoundError, } from 'graphql-config'; @@ -88,24 +87,22 @@ function toPosition(position: VscodePosition): IPosition { } export class MessageProcessor { - _connection: Connection; - _graphQLCache!: GraphQLCache; - _graphQLConfig: GraphQLConfig | undefined; - _languageService!: GraphQLLanguageService; - _textDocumentCache = new Map(); - _isInitialized = false; - _isGraphQLConfigMissing: boolean | null = null; - _willShutdown = false; - _logger: Logger | NoopLogger; - _extensions?: GraphQLExtensionDeclaration[]; - _parser: (text: string, uri: string) => CachedContent[]; - _tmpDir: string; - _tmpUriBase: string; - _tmpDirBase: string; - _loadConfigOptions: LoadConfigOptions; - _schemaCacheInit = false; - _rootPath: string = process.cwd(); - _settings: any; + private _connection: Connection; + private _graphQLCache!: GraphQLCache; + private _languageService!: GraphQLLanguageService; + private _textDocumentCache = new Map(); + private _isInitialized = false; + private _isGraphQLConfigMissing: boolean | null = null; + private _willShutdown = false; + private _logger: Logger | NoopLogger; + private _parser: (text: string, uri: string) => CachedContent[]; + private _tmpDir: string; + private _tmpUriBase: string; + private _tmpDirBase: string; + private _loadConfigOptions: LoadConfigOptions; + private _schemaCacheInit = false; + private _rootPath: string = process.cwd(); + private _settings: any; constructor({ logger, @@ -128,7 +125,6 @@ export class MessageProcessor { }) { this._connection = connection; this._logger = logger; - this._graphQLConfig = config; this._parser = (text, uri) => { const p = parser ?? parseDocument; return p(text, uri, fileExtensions, graphqlFileExtensions, this._logger); @@ -138,12 +134,6 @@ export class MessageProcessor { this._tmpUriBase = URI.file(this._tmpDirBase).toString(); // use legacy mode by default for backwards compatibility this._loadConfigOptions = { legacy: true, ...loadConfigOptions }; - if ( - loadConfigOptions.extensions && - loadConfigOptions.extensions?.length > 0 - ) { - this._extensions = loadConfigOptions.extensions; - } if (!existsSync(this._tmpDirBase)) { void mkdirSync(this._tmpDirBase); @@ -156,7 +146,7 @@ export class MessageProcessor { this._connection = connection; } - async handleInitializeRequest( + public async handleInitializeRequest( params: InitializeParams, _token?: CancellationToken, configDir?: string, @@ -244,7 +234,6 @@ export class MessageProcessor { ); const config = this._graphQLCache.getGraphQLConfig(); if (config) { - this._graphQLConfig = config; await this._cacheAllProjectFiles(config); this._isInitialized = true; this._isGraphQLConfigMissing = false; @@ -312,29 +301,44 @@ export class MessageProcessor { } return false; } - - async handleDidOpenOrSaveNotification( - params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, - ): Promise { - /** - * Initialize the LSP server when the first file is opened or saved, - * so that we can access the user settings for config rootDir, etc - */ - const isGraphQLConfigFile = await this._isGraphQLConfigFile( - params.textDocument.uri, - ); + private async _loadConfigOrSkip(uri: string) { try { + const isGraphQLConfigFile = await this._isGraphQLConfigFile(uri); + if (!this._isInitialized) { - // don't try to initialize again if we've already tried - // and the graphql config file or package.json entry isn't even there if (this._isGraphQLConfigMissing === true && !isGraphQLConfigFile) { - return { uri: params.textDocument.uri, diagnostics: [] }; + return true; } - // then initial call to update graphql config + // don't try to initialize again if we've already tried + // and the graphql config file or package.json entry isn't even there + await this._updateGraphQLConfig(); + return isGraphQLConfigFile; + } + // if it has initialized, but this is another config file change, then let's handle it + if (isGraphQLConfigFile) { await this._updateGraphQLConfig(); } + return isGraphQLConfigFile; } catch (err) { this._logger.error(String(err)); + // return true if it's a graphql config file so we don't treat + // this as a non-config file if it is one + return true; + } + } + + public async handleDidOpenOrSaveNotification( + params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, + ): Promise { + /** + * Initialize the LSP server when the first file is opened or saved, + * so that we can access the user settings for config rootDir, etc + */ + const shouldSkip = await this._loadConfigOrSkip(params.textDocument.uri); + // if we're loading config or the config is missing or there's an error + // don't do anything else + if (shouldSkip) { + return { uri: params.textDocument.uri, diagnostics: [] }; } // Here, we set the workspace settings in memory, @@ -361,20 +365,13 @@ export class MessageProcessor { contents = this._parser(text, uri); await this._invalidateCache(textDocument, uri, contents); - } else if (isGraphQLConfigFile) { - this._logger.info('updating graphql config'); - await this._updateGraphQLConfig(); - return { uri, diagnostics: [] }; } if (!this._graphQLCache) { return { uri, diagnostics }; } try { const project = this._graphQLCache.getProjectForFile(uri); - if ( - this._isInitialized && - project?.extensions?.languageService?.enableValidation !== false - ) { + if (project?.extensions?.languageService?.enableValidation !== false) { await Promise.all( contents.map(async ({ query, range }) => { const results = await this._languageService.getDiagnostics( @@ -406,7 +403,7 @@ export class MessageProcessor { return { uri, diagnostics }; } - async handleDidChangeNotification( + public async handleDidChangeNotification( params: DidChangeTextDocumentParams, ): Promise { if ( @@ -497,7 +494,7 @@ export class MessageProcessor { return {}; } - handleDidCloseNotification(params: DidCloseTextDocumentParams): void { + public handleDidCloseNotification(params: DidCloseTextDocumentParams): void { if (!this._isInitialized) { return; } @@ -525,11 +522,11 @@ export class MessageProcessor { ); } - handleShutdownRequest(): void { + public handleShutdownRequest(): void { this._willShutdown = true; } - handleExitNotification(): void { + public handleExitNotification(): void { process.exit(this._willShutdown ? 0 : 1); } @@ -541,7 +538,7 @@ export class MessageProcessor { } } - async handleCompletionRequest( + public async handleCompletionRequest( params: CompletionParams, ): Promise { if (!this._isInitialized) { @@ -599,7 +596,9 @@ export class MessageProcessor { return { items: result, isIncomplete: false }; } - async handleHoverRequest(params: TextDocumentPositionParams): Promise { + public async handleHoverRequest( + params: TextDocumentPositionParams, + ): Promise { if (!this._isInitialized) { return { contents: [] }; } @@ -642,7 +641,7 @@ export class MessageProcessor { }; } - async handleWatchedFilesChangedNotification( + public async handleWatchedFilesChangedNotification( params: DidChangeWatchedFilesParams, ): Promise | null> { if ( @@ -655,13 +654,9 @@ export class MessageProcessor { return Promise.all( params.changes.map(async (change: FileEvent) => { - if ( - this._isGraphQLConfigMissing || - !this._isInitialized || - !this._graphQLCache - ) { - this._logger.warn('No cache available for handleWatchedFilesChanged'); - return; + const shouldSkip = await this._loadConfigOrSkip(change.uri); + if (shouldSkip) { + return { uri: change.uri, diagnostics: [] }; } if ( change.type === FileChangeTypeKind.Created || @@ -731,7 +726,7 @@ export class MessageProcessor { ); } - async handleDefinitionRequest( + public async handleDefinitionRequest( params: TextDocumentPositionParams, _token?: CancellationToken, ): Promise> { @@ -836,7 +831,7 @@ export class MessageProcessor { return formatted; } - async handleDocumentSymbolRequest( + public async handleDocumentSymbolRequest( params: DocumentSymbolParams, ): Promise> { if (!this._isInitialized) { @@ -896,7 +891,7 @@ export class MessageProcessor { // ); // } - async handleWorkspaceSymbolRequest( + public async handleWorkspaceSymbolRequest( params: WorkspaceSymbolParams, ): Promise> { if (!this._isInitialized) { diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 3b6eb14b4ba..34b1eabd5bc 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -42,8 +42,7 @@ describe('MessageProcessor with no config', () => { files: [...defaultFiles, ['graphql.config.json', '']], }); await project.init(); - expect(project.lsp._isInitialized).toEqual(false); - expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + expect(project.lsp._logger.info).toHaveBeenCalledTimes(1); expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); expect(project.lsp._logger.error).toHaveBeenCalledWith( @@ -51,20 +50,23 @@ describe('MessageProcessor with no config', () => { /GraphQL Config file is not available in the provided config directory/, ), ); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); }); it('fails to initialize with no config file present', async () => { const project = new MockProject({ files: [...defaultFiles], }); await project.init(); - expect(project.lsp._isInitialized).toEqual(false); - expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); expect(project.lsp._logger.error).toHaveBeenCalledWith( expect.stringMatching( /GraphQL Config file is not available in the provided config directory/, ), ); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); }); it('initializes when presented with a valid config later', async () => { const project = new MockProject({ @@ -246,9 +248,8 @@ describe('project with simple config and graphql files', () => { fooLaterTypePosition, ); expect(project.lsp._logger.error).not.toHaveBeenCalled(); - - // TODO: the position should change when a watched file changes??? }); + it('caches files and schema with a URL config', async () => { const project = new MockProject({ files: [ @@ -262,10 +263,10 @@ describe('project with simple config and graphql files', () => { }); const initParams = await project.init('query.graphql'); - expect(initParams.diagnostics).toEqual([]); - expect(project.lsp._logger.error).not.toHaveBeenCalled(); + expect(initParams.diagnostics).toEqual([]); + const changeParams = await project.lsp.handleDidChangeNotification({ textDocument: { uri: project.uri('query.graphql'), version: 1 }, contentChanges: [{ text: 'query { test { isTest, ...T or } }' }], @@ -373,6 +374,7 @@ describe('project with simple config and graphql files', () => { }); expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); + it('caches multiple projects with files and schema with a URL config and a local schema', async () => { const project = new MockProject({ files: [ 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 ce36f32c4bd..9c0d22ce34f 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -548,6 +548,54 @@ describe('MessageProcessor', () => { }); }); + describe('_loadConfigOrSkip', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._updateGraphQLConfig = jest.fn(); + }); + + it('loads config if not initialized', async () => { + messageProcessor._isInitialized = false; + + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/graphql.config.js`, + ); + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalledTimes(1); + // we want to return true here to skip further processing, because it's just a config file change + expect(result).toEqual(true); + }); + + it('loads config if a file change occurs and the server is not initialized', async () => { + messageProcessor._isInitialized = false; + + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/file.ts`, + ); + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalledTimes(1); + // here we have a non-config file, so we don't want to skip, because we need to run diagnostics etc + expect(result).toEqual(false); + }); + it('config file change updates server config even if the server is already initialized', async () => { + messageProcessor._isInitialized = true; + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/graphql.config.ts`, + ); + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalledTimes(1); + expect(result).toEqual(true); + }); + it('skips if the server is already initialized', async () => { + messageProcessor._isInitialized = true; + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/myFile.ts`, + ); + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(result).toEqual(false); + }); + }); + describe('handleDidOpenOrSaveNotification', () => { const mockReadFileSync: jest.Mock = jest.requireMock('node:fs').readFileSync; @@ -555,6 +603,7 @@ describe('MessageProcessor', () => { beforeEach(() => { mockReadFileSync.mockReturnValue(''); messageProcessor._updateGraphQLConfig = jest.fn(); + messageProcessor._loadConfigOrSkip = jest.fn(); }); it('updates config for standard config filename changes', async () => { await messageProcessor.handleDidOpenOrSaveNotification({ @@ -565,8 +614,7 @@ describe('MessageProcessor', () => { text: '', }, }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + expect(messageProcessor._loadConfigOrSkip).toHaveBeenCalled(); }); it('updates config for custom config filename changes', async () => { @@ -582,7 +630,9 @@ describe('MessageProcessor', () => { }, }); - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + expect(messageProcessor._loadConfigOrSkip).toHaveBeenCalledWith( + expect.stringContaining(customConfigName), + ); }); it('handles config requests with no config', async () => { @@ -606,6 +656,7 @@ describe('MessageProcessor', () => { expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); }); }); + describe('_handleConfigErrors', () => { it('handles missing config errors', async () => { messageProcessor._handleConfigError({ @@ -698,7 +749,6 @@ describe('MessageProcessor', () => { beforeEach(() => { mockReadFileSync.mockReturnValue(''); - messageProcessor._graphQLConfig = undefined; messageProcessor._isGraphQLConfigMissing = true; messageProcessor._parser = jest.fn(); }); @@ -722,7 +772,6 @@ describe('MessageProcessor', () => { beforeEach(() => { mockReadFileSync.mockReturnValue(''); - messageProcessor._graphQLConfig = undefined; messageProcessor._isGraphQLConfigMissing = true; messageProcessor._parser = jest.fn(); }); From 867714ce45ca627c6023a0d9895823584c527869 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 3 Mar 2024 10:31:22 +0100 Subject: [PATCH 39/49] fix even MORE bugs --- .../src/MessageProcessor.ts | 277 ++++++++++-------- .../src/__tests__/MessageProcessor.spec.ts | 59 +++- .../src/__tests__/MessageProcessor.test.ts | 71 +++-- .../src/__tests__/__utils__/MockProject.ts | 5 +- .../src/parsers/astro.ts | 14 +- .../src/parsers/babel.ts | 24 +- .../src/parsers/svelte.ts | 12 +- .../src/parsers/vue.ts | 14 +- 8 files changed, 290 insertions(+), 186 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index a2ba7b969df..48f67a26afa 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -97,12 +97,11 @@ export class MessageProcessor { private _logger: Logger | NoopLogger; private _parser: (text: string, uri: string) => CachedContent[]; private _tmpDir: string; - private _tmpUriBase: string; private _tmpDirBase: string; private _loadConfigOptions: LoadConfigOptions; - private _schemaCacheInit = false; private _rootPath: string = process.cwd(); private _settings: any; + private _providedConfig?: GraphQLConfig; constructor({ logger, @@ -123,6 +122,9 @@ export class MessageProcessor { tmpDir?: string; connection: Connection; }) { + if (config) { + this._providedConfig = config; + } this._connection = connection; this._logger = logger; this._parser = (text, uri) => { @@ -131,7 +133,6 @@ export class MessageProcessor { }; this._tmpDir = tmpDir || tmpdir(); this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service'); - this._tmpUriBase = URI.file(this._tmpDirBase).toString(); // use legacy mode by default for backwards compatibility this._loadConfigOptions = { legacy: true, ...loadConfigOptions }; @@ -193,8 +194,8 @@ export class MessageProcessor { return serverCapabilities; } - - async _updateGraphQLConfig() { + // TODO next: refactor (most of) this into the `GraphQLCache` class + async _initializeGraphQLCaches() { const settings = await this._connection.workspace.getConfiguration({ section: 'graphql-config', }); @@ -205,6 +206,8 @@ export class MessageProcessor { if (settings?.dotEnvPath) { require('dotenv').config({ path: settings.dotEnvPath }); } + // TODO: eventually we will instantiate an instance of this per workspace, + // so rootDir should become that workspace's rootDir this._settings = { ...settings, ...vscodeSettings }; const rootDir = this._settings?.load?.rootDir.length ? this._settings?.load?.rootDir @@ -220,18 +223,35 @@ export class MessageProcessor { }, this._settings.load ?? {}), rootDir, }; + try { - // reload the graphql cache - this._graphQLCache = await getGraphQLCache({ - parser: this._parser, - loadConfigOptions: this._loadConfigOptions, + // createServer() can be called with a custom config object, and + // this is a public interface that may be used by customized versions of the server + if (this._providedConfig) { + this._graphQLCache = new GraphQLCache({ + config: this._providedConfig, + logger: this._logger, + parser: this._parser, + configDir: rootDir, + }); + this._languageService = new GraphQLLanguageService( + this._graphQLCache, + this._logger, + ); + } else { + // reload the graphql cache + this._graphQLCache = await getGraphQLCache({ + parser: this._parser, + loadConfigOptions: this._loadConfigOptions, + + logger: this._logger, + }); + this._languageService = new GraphQLLanguageService( + this._graphQLCache, + this._logger, + ); + } - logger: this._logger, - }); - this._languageService = new GraphQLLanguageService( - this._graphQLCache, - this._logger, - ); const config = this._graphQLCache.getGraphQLConfig(); if (config) { await this._cacheAllProjectFiles(config); @@ -243,7 +263,6 @@ export class MessageProcessor { } } private _handleConfigError({ err }: { err: unknown; uri?: string }) { - // console.log(err, typeof err); if (err instanceof ConfigNotFoundError || err instanceof ConfigEmptyError) { // TODO: obviously this needs to become a map by workspace from uri // for workspaces support @@ -311,12 +330,12 @@ export class MessageProcessor { } // don't try to initialize again if we've already tried // and the graphql config file or package.json entry isn't even there - await this._updateGraphQLConfig(); + await this._initializeGraphQLCaches(); return isGraphQLConfigFile; } // if it has initialized, but this is another config file change, then let's handle it if (isGraphQLConfigFile) { - await this._updateGraphQLConfig(); + await this._initializeGraphQLCaches(); } return isGraphQLConfigFile; } catch (err) { @@ -330,15 +349,18 @@ export class MessageProcessor { public async handleDidOpenOrSaveNotification( params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, ): Promise { + const { textDocument } = params; + const { uri } = textDocument; + /** * Initialize the LSP server when the first file is opened or saved, * so that we can access the user settings for config rootDir, etc */ - const shouldSkip = await this._loadConfigOrSkip(params.textDocument.uri); + const shouldSkip = await this._loadConfigOrSkip(uri); // if we're loading config or the config is missing or there's an error // don't do anything else if (shouldSkip) { - return { uri: params.textDocument.uri, diagnostics: [] }; + return { uri, diagnostics: [] }; } // Here, we set the workspace settings in memory, @@ -347,45 +369,49 @@ export class MessageProcessor { // We aren't able to use initialization event for this // and the config change event is after the fact. - if (!params?.textDocument) { + if (!textDocument) { throw new Error('`textDocument` argument is required.'); } - const { textDocument } = params; - const { uri } = textDocument; const diagnostics: Diagnostic[] = []; - let contents: CachedContent[] = []; - const text = 'text' in textDocument && textDocument.text; - // Create/modify the cached entry if text is provided. - // Otherwise, try searching the cache to perform diagnostics. - if (text) { - // textDocument/didSave does not pass in the text content. - // Only run the below function if text is passed in. - contents = this._parser(text, uri); - - await this._invalidateCache(textDocument, uri, contents); - } - if (!this._graphQLCache) { + if (!this._isInitialized) { return { uri, diagnostics }; } try { const project = this._graphQLCache.getProjectForFile(uri); - if (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), - ); - } - }), + + if (project) { + const text = 'text' in textDocument && textDocument.text; + // for some reason if i try to tell to not parse empty files, it breaks :shrug: + // i think this is because if the file change is empty, it doesn't get parsed + // TODO: this could be related to a bug in how we are calling didOpenOrSave in our tests + // that doesn't reflect the real runtime behavior + // if (!text || text.length < 1) { + // return { uri, diagnostics }; + // } + + const { contents } = await this._parseAndCacheFile( + uri, + project, + text as string, ); + if (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), + ); + } + }), + ); + } } this._logger.log( @@ -396,11 +422,11 @@ export class MessageProcessor { fileName: uri, }), ); + return { uri, diagnostics }; } catch (err) { this._handleConfigError({ err, uri }); + return { uri, diagnostics }; } - - return { uri, diagnostics }; } public async handleDidChangeNotification( @@ -424,27 +450,25 @@ export class MessageProcessor { } const { textDocument, contentChanges } = params; const { uri } = textDocument; - const project = this._graphQLCache.getProjectForFile(uri); + try { - const contentChange = contentChanges.at(-1)!; + const project = this._graphQLCache.getProjectForFile(uri); + if (!project) { + return { uri, diagnostics: [] }; + } // As `contentChanges` is an array, and we just want the // latest update to the text, grab the last entry from the array. // 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 = this._parser(contentChange.text, uri); - // If it's a .graphql file, proceed normally and invalidate the cache. - await this._invalidateCache(textDocument, uri, contents); - - const cachedDocument = this._getCachedDocument(uri); - - if (!cachedDocument) { - return null; - } - - await this._updateFragmentDefinition(uri, contents); - await this._updateObjectTypeDefinition(uri, contents); + const { contents } = await this._parseAndCacheFile( + uri, + project, + contentChanges.at(-1)!.text, + ); + // // If it's a .graphql file, proceed normally and invalidate the cache. + // await this._invalidateCache(textDocument, uri, contents); const diagnostics: Diagnostic[] = []; @@ -484,7 +508,7 @@ export class MessageProcessor { async handleDidChangeConfiguration( _params: DidChangeConfigurationParams, ): Promise { - await this._updateGraphQLConfig(); + await this._initializeGraphQLCaches(); this._logger.log( JSON.stringify({ type: 'usage', @@ -641,18 +665,30 @@ export class MessageProcessor { }; } + private async _parseAndCacheFile( + uri: string, + project: GraphQLProjectConfig, + text?: string, + ) { + try { + const fileText = text || (await readFile(URI.parse(uri).fsPath, 'utf-8')); + const contents = this._parser(fileText, uri); + const cachedDocument = this._textDocumentCache.get(uri); + const version = cachedDocument ? cachedDocument.version++ : 0; + await this._invalidateCache({ uri, version }, uri, contents); + await this._updateFragmentDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents, project); + await this._updateSchemaIfChanged(project, uri); + return { contents, version }; + } catch { + return { contents: [], version: 0 }; + } + } + public async handleWatchedFilesChangedNotification( params: DidChangeWatchedFilesParams, ): Promise | null> { - if ( - this._isGraphQLConfigMissing || - !this._isInitialized || - !this._graphQLCache - ) { - return null; - } - - return Promise.all( + const resultsForChanges = Promise.all( params.changes.map(async (change: FileEvent) => { const shouldSkip = await this._loadConfigOrSkip(change.uri); if (shouldSkip) { @@ -664,59 +700,39 @@ export class MessageProcessor { ) { const { uri } = change; - const text = await readFile(URI.parse(uri).fsPath, 'utf-8'); - const contents = this._parser(text, uri); - const cachedDocument = this._textDocumentCache.get(uri); - const version = cachedDocument ? cachedDocument.version++ : 0; - await this._invalidateCache( - { uri, version }, - URI.parse(uri).fsPath, - contents, - ); - await this._updateFragmentDefinition(uri, contents); - await this._updateObjectTypeDefinition(uri, contents); - try { + let diagnostics: Diagnostic[] = []; const project = this._graphQLCache.getProjectForFile(uri); if (project) { - await this._updateSchemaIfChanged(project, 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); - } - return []; - }), - ) - ).reduce((left, right) => left.concat(right), diagnostics); + // Important! Use system file uri not file path here!!!! + const { contents } = await this._parseAndCacheFile(uri, project); + 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); + } + + return { uri, diagnostics }; } - - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'workspace/didChangeWatchedFiles', - projectName: project?.name, - fileName: uri, - }), - ); - return { uri, diagnostics }; } catch (err) { this._handleConfigError({ err, uri }); - return { uri, diagnostics: [] }; } + return { uri, diagnostics: [] }; } if (change.type === FileChangeTypeKind.Deleted) { await this._updateFragmentDefinition(change.uri, []); @@ -724,6 +740,14 @@ export class MessageProcessor { } }), ); + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'workspace/didChangeWatchedFiles', + files: params.changes.map(change => change.uri), + }), + ); + return resultsForChanges; } public async handleDefinitionRequest( @@ -739,9 +763,6 @@ export class MessageProcessor { } const { textDocument, position } = params; const project = this._graphQLCache.getProjectForFile(textDocument.uri); - if (project) { - await this._cacheSchemaFilesForProject(project); - } const cachedDocument = this._getCachedDocument(textDocument.uri); if (!cachedDocument) { return []; @@ -897,8 +918,6 @@ export class MessageProcessor { if (!this._isInitialized) { return []; } - // const config = await this._graphQLCache.getGraphQLConfig(); - // await this._cacheAllProjectFiles(config); if (params.query !== '') { const documents = this._getTextDocuments(); @@ -937,13 +956,12 @@ export class MessageProcessor { ) { try { const contents = this._parser(text, uri); - // console.log(uri, contents); if (contents.length > 0) { await this._invalidateCache({ version, uri }, uri, contents); await this._updateObjectTypeDefinition(uri, contents, project); } - } catch (err) { - this._logger.error(String(err)); + } catch { + // this._logger.error(String(err)); } } private async _cacheSchemaFile( @@ -952,7 +970,6 @@ export class MessageProcessor { ) { try { // const parsedUri = URI.file(fileUri.toString()); - // console.log(readdirSync(project.dirpath), fileUri.toString()); // @ts-expect-error const matches = await glob(fileUri, { cwd: project.dirpath, diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 34b1eabd5bc..85f19a20a4f 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -116,7 +116,13 @@ describe('project with simple config and graphql files', () => { ...defaultFiles, ], }); - await project.init('query.graphql'); + const results = await project.init('query.graphql'); + expect(results.diagnostics[0].message).toEqual( + 'Cannot query field "bar" on type "Query".', + ); + expect(results.diagnostics[1].message).toEqual( + 'Fragment "B" cannot be spread here as objects of type "Query" can never be of type "Foo".', + ); const initSchemaDefRequest = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('schema.graphql') }, position: { character: 19, line: 0 }, @@ -190,11 +196,52 @@ describe('project with simple config and graphql files', () => { fooLaterTypePosition, ); + // change the file to make the fragment invalid + project.changeFile( + 'schema.graphql', + // now Foo has a bad field, the fragment should be invalid + 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', + ); + // await project.lsp.handleWatchedFilesChangedNotification({ + // changes: [ + // { + // type: FileChangeType.Changed, + // uri: project.uri('schema.graphql'), + // }, + // ], + // }); + await project.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeType.Changed, + text: 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', + }, + ], + textDocument: { uri: project.uri('schema.graphql'), version: 1 }, + }); + + const schemaDefRequest2 = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('schema.graphql') }, + position: { character: 19, line: 0 }, + }); + + const fooLaterTypePosition2 = { + start: { line: 8, character: 0 }, + end: { line: 8, character: 21 }, + }; + expect(schemaDefRequest2.length).toEqual(1); + expect(schemaDefRequest2[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(schemaDefRequest2[0].range)).toEqual( + fooLaterTypePosition2, + ); + // TODO: this fragment should now be invalid const result = await project.lsp.handleDidOpenOrSaveNotification({ textDocument: { uri: project.uri('fragments.graphql') }, }); - expect(result.diagnostics).toEqual([]); + expect(result.diagnostics[0].message).toEqual( + 'Cannot query field "bar" on type "Foo". Did you mean "bad"?', + ); const generatedFile = existsSync(join(genSchemaPath)); // this generated file should not exist because the schema is local! expect(generatedFile).toEqual(false); @@ -245,7 +292,7 @@ describe('project with simple config and graphql files', () => { ); expect(serializeRange(schemaDefinitionsAgain[0].range)).toEqual( - fooLaterTypePosition, + fooLaterTypePosition2, ); expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); @@ -384,7 +431,7 @@ describe('project with simple config and graphql files', () => { ], [ 'a/query.ts', - '\n\n\nexport const query = gql`query { test() { isTest, ...T } }`', + '\n\n\nexport const query = graphql`query { test { isTest ...T } }`', ], [ @@ -408,8 +455,8 @@ describe('project with simple config and graphql files', () => { ], }); - const initParams = await project.init('a/query.graphql'); - expect(initParams.diagnostics).toEqual([]); + const initParams = await project.init('a/query.ts'); + expect(initParams.diagnostics[0].message).toEqual('Unknown fragment "T".'); expect(project.lsp._logger.error).not.toHaveBeenCalled(); expect(await project.lsp._graphQLCache.getSchema('a')).toBeDefined(); 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 9c0d22ce34f..e58b5979f01 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -48,6 +48,7 @@ describe('MessageProcessor', () => { logger, graphqlFileExtensions: ['graphql'], loadConfigOptions: { rootDir: __dirname }, + config: null, }); const queryPathUri = pathToFileURL(`${__dirname}/__queries__`); @@ -57,9 +58,10 @@ describe('MessageProcessor', () => { } } `; - + let gqlConfig; beforeEach(async () => { - const gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); + gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); + // loadConfig.mockRestore(); messageProcessor._settings = { load: {} }; messageProcessor._graphQLCache = new GraphQLCache({ @@ -460,6 +462,35 @@ describe('MessageProcessor', () => { const result = await messageProcessor.handleHoverRequest(test); expect(result).toEqual({ contents: [] }); }); + it('handles provided config', async () => { + const msgProcessor = new MessageProcessor({ + // @ts-ignore + connection: { + workspace: { + getConfiguration() { + return {}; + }, + }, + }, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + config: gqlConfig, + }); + expect(msgProcessor._providedConfig).toBeTruthy(); + await msgProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + await msgProcessor.handleDidChangeConfiguration({ + settings: {}, + }); + expect(msgProcessor._graphQLCache).toBeTruthy(); + }); it('runs workspace symbol requests', async () => { const msgProcessor = new MessageProcessor({ @@ -554,7 +585,7 @@ describe('MessageProcessor', () => { beforeEach(() => { mockReadFileSync.mockReturnValue(''); - messageProcessor._updateGraphQLConfig = jest.fn(); + messageProcessor._initializeGraphQLCaches = jest.fn(); }); it('loads config if not initialized', async () => { @@ -563,7 +594,9 @@ describe('MessageProcessor', () => { const result = await messageProcessor._loadConfigOrSkip( `${pathToFileURL('.')}/graphql.config.js`, ); - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalledTimes(1); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalledTimes( + 1, + ); // we want to return true here to skip further processing, because it's just a config file change expect(result).toEqual(true); }); @@ -574,7 +607,9 @@ describe('MessageProcessor', () => { const result = await messageProcessor._loadConfigOrSkip( `${pathToFileURL('.')}/file.ts`, ); - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalledTimes(1); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalledTimes( + 1, + ); // here we have a non-config file, so we don't want to skip, because we need to run diagnostics etc expect(result).toEqual(false); }); @@ -583,7 +618,9 @@ describe('MessageProcessor', () => { const result = await messageProcessor._loadConfigOrSkip( `${pathToFileURL('.')}/graphql.config.ts`, ); - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalledTimes(1); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalledTimes( + 1, + ); expect(result).toEqual(true); }); it('skips if the server is already initialized', async () => { @@ -591,7 +628,7 @@ describe('MessageProcessor', () => { const result = await messageProcessor._loadConfigOrSkip( `${pathToFileURL('.')}/myFile.ts`, ); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(result).toEqual(false); }); }); @@ -602,7 +639,7 @@ describe('MessageProcessor', () => { beforeEach(() => { mockReadFileSync.mockReturnValue(''); - messageProcessor._updateGraphQLConfig = jest.fn(); + messageProcessor._initializeGraphQLCaches = jest.fn(); messageProcessor._loadConfigOrSkip = jest.fn(); }); it('updates config for standard config filename changes', async () => { @@ -642,7 +679,7 @@ describe('MessageProcessor', () => { settings: [], }); - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalled(); await messageProcessor.handleDidOpenOrSaveNotification({ textDocument: { @@ -653,7 +690,7 @@ describe('MessageProcessor', () => { }, }); - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalled(); }); }); @@ -664,7 +701,7 @@ describe('MessageProcessor', () => { uri: 'test', }); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining('test missing-config'), ); @@ -675,7 +712,7 @@ describe('MessageProcessor', () => { uri: 'test', }); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining('Project not found for this file'), ); @@ -686,7 +723,7 @@ describe('MessageProcessor', () => { uri: 'test', }); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining('Invalid configuration'), ); @@ -697,7 +734,7 @@ describe('MessageProcessor', () => { uri: 'test', }); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining('test loader-error'), ); @@ -708,7 +745,7 @@ describe('MessageProcessor', () => { uri: 'test', }); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining('test loader-error'), ); @@ -720,7 +757,7 @@ describe('MessageProcessor', () => { beforeEach(() => { mockReadFileSync.mockReturnValue(' query { id }'); - messageProcessor._updateGraphQLConfig = jest.fn(); + messageProcessor._initializeGraphQLCaches = jest.fn(); messageProcessor._updateFragmentDefinition = jest.fn(); messageProcessor._isGraphQLConfigMissing = false; messageProcessor._isInitialized = true; @@ -738,7 +775,7 @@ describe('MessageProcessor', () => { ], }); - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); expect(messageProcessor._updateFragmentDefinition).toHaveBeenCalled(); }); }); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts index 6cd215a9424..c9a86532c32 100644 --- a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -86,7 +86,10 @@ export class MockProject { textDocument: { uri: this.uri(filename || 'query.graphql'), version: 1, - text: this.fileCache.get('query.graphql') || fileText, + text: + this.fileCache.get('query.graphql') || + (filename && this.fileCache.get(filename)) || + fileText, }, }); } diff --git a/packages/graphql-language-service-server/src/parsers/astro.ts b/packages/graphql-language-service-server/src/parsers/astro.ts index 0b870fdaa30..b09a315d11f 100644 --- a/packages/graphql-language-service-server/src/parsers/astro.ts +++ b/packages/graphql-language-service-server/src/parsers/astro.ts @@ -41,15 +41,15 @@ function parseAstro(source: string): ParseAstroResult { return { type: 'error', errors: ['Could not find frontmatter block'] }; } -export const astroParser: SourceParser = (text, uri, logger) => { +export const astroParser: SourceParser = (text, _uri, _logger) => { const parseAstroResult = parseAstro(text); if (parseAstroResult.type === 'error') { - logger.error( - `Could not parse the astro file at ${uri} to extract the graphql tags:`, - ); - for (const error of parseAstroResult.errors) { - logger.error(String(error)); - } + // logger.error( + // `Could not parse the astro file at ${uri} to extract the graphql tags:`, + // ); + // for (const error of parseAstroResult.errors) { + // logger.error(String(error)); + // } return null; } diff --git a/packages/graphql-language-service-server/src/parsers/babel.ts b/packages/graphql-language-service-server/src/parsers/babel.ts index aa2c37bd33a..05887352fd7 100644 --- a/packages/graphql-language-service-server/src/parsers/babel.ts +++ b/packages/graphql-language-service-server/src/parsers/babel.ts @@ -11,26 +11,26 @@ export const babelParser = (text: string, plugins?: ParserPlugin[]) => { return parse(text, PARSER_OPTIONS); }; -export const ecmaParser: SourceParser = (text, uri, logger) => { +export const ecmaParser: SourceParser = (text, _uri, _logger) => { try { return { asts: [babelParser(text, ['flow', 'flowComments'])] }; - } catch (error) { - logger.error( - `Could not parse the JavaScript file at ${uri} to extract the graphql tags:`, - ); - logger.error(String(error)); + } catch { + // logger.error( + // `Could not parse the JavaScript file at ${uri} to extract the graphql tags:`, + // ); + // logger.error(String(error)); return null; } }; -export const tsParser: SourceParser = (text, uri, logger) => { +export const tsParser: SourceParser = (text, _uri, _logger) => { try { return { asts: [babelParser(text, ['typescript'])] }; - } catch (error) { - logger.error( - `Could not parse the TypeScript file at ${uri} to extract the graphql tags:`, - ); - logger.error(String(error)); + } catch { + // logger.error( + // `Could not parse the TypeScript file at ${uri} to extract the graphql tags:`, + // ); + // logger.error(String(error)); return null; } }; diff --git a/packages/graphql-language-service-server/src/parsers/svelte.ts b/packages/graphql-language-service-server/src/parsers/svelte.ts index f19178b6239..4a1e9b9c3eb 100644 --- a/packages/graphql-language-service-server/src/parsers/svelte.ts +++ b/packages/graphql-language-service-server/src/parsers/svelte.ts @@ -4,7 +4,7 @@ import { SourceMapConsumer } from 'source-map-js'; import { Position, Range } from 'graphql-language-service'; import type { RangeMapper, SourceParser } from './types'; -export const svelteParser: SourceParser = (text, uri, logger) => { +export const svelteParser: SourceParser = (text, uri, _logger) => { const svelteResult = svelte2tsx(text, { filename: uri, }); @@ -35,11 +35,11 @@ export const svelteParser: SourceParser = (text, uri, logger) => { asts: [babelParser(svelteResult.code, ['typescript'])], rangeMapper, }; - } catch (error) { - logger.error( - `Could not parse the Svelte file at ${uri} to extract the graphql tags:`, - ); - logger.error(String(error)); + } catch { + // logger.error( + // `Could not parse the Svelte file at ${uri} to extract the graphql tags:`, + // ); + // logger.error(String(error)); return null; } }; diff --git a/packages/graphql-language-service-server/src/parsers/vue.ts b/packages/graphql-language-service-server/src/parsers/vue.ts index cdcb30de263..b982012c2db 100644 --- a/packages/graphql-language-service-server/src/parsers/vue.ts +++ b/packages/graphql-language-service-server/src/parsers/vue.ts @@ -45,16 +45,16 @@ export function parseVueSFC(source: string): ParseVueSFCResult { }; } -export const vueParser: SourceParser = (text, uri, logger) => { +export const vueParser: SourceParser = (text, _uri, _logger) => { const asts = []; const parseVueSFCResult = parseVueSFC(text); if (parseVueSFCResult.type === 'error') { - logger.error( - `Could not parse the vue file at ${uri} to extract the graphql tags:`, - ); - for (const error of parseVueSFCResult.errors) { - logger.error(String(error)); - } + // logger.error( + // `Could not parse the vue file at ${uri} to extract the graphql tags:`, + // ); + // for (const error of parseVueSFCResult.errors) { + // logger.error(String(error)); + // } return null; } From 9b6304c0628b3653080686c342bc9888c75781be Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 3 Mar 2024 17:29:30 +0100 Subject: [PATCH 40/49] fix object field completion, add tests for the missing cases --- .../src/GraphQLCache.ts | 12 ++-- .../src/GraphQLLanguageService.ts | 48 +++++++------- .../src/MessageProcessor.ts | 43 +++++++----- .../src/__tests__/MessageProcessor.spec.ts | 65 ++++++++++--------- .../getAutocompleteSuggestions-test.ts | 52 +++++++++++++++ .../interface/getAutocompleteSuggestions.ts | 49 ++++++-------- 6 files changed, 165 insertions(+), 104 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 224a6cf2636..0f181ed0071 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -20,7 +20,6 @@ import { } from 'graphql'; import type { CachedContent, - GraphQLCache as GraphQLCacheInterface, GraphQLFileMetadata, GraphQLFileInfo, FragmentInfo, @@ -93,7 +92,7 @@ export async function getGraphQLCache({ }); } -export class GraphQLCache implements GraphQLCacheInterface { +export class GraphQLCache { _configDir: Uri; _graphQLFileListCache: Map>; _graphQLConfig: GraphQLConfig; @@ -596,8 +595,13 @@ export class GraphQLCache implements GraphQLCacheInterface { if (schemaPath && schemaKey) { schemaCacheKey = schemaKey as string; - // Read from disk - schema = await projectConfig.getSchema(); + try { + // Read from disk + schema = await projectConfig.getSchema(); + } catch { + // // if there is an error reading the schema, just use the last valid schema + // schema = this._schemaMap.get(schemaCacheKey); + } if (this._schemaMap.has(schemaCacheKey)) { schema = this._schemaMap.get(schemaCacheKey); diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts index aeaa4c92a8e..e0392f285c1 100644 --- a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -29,7 +29,6 @@ import { IPosition, Outline, OutlineTree, - GraphQLCache, getAutocompleteSuggestions, getHoverInformation, HoverConfig, @@ -47,6 +46,8 @@ import { getTypeInfo, } from 'graphql-language-service'; +import type { GraphQLCache } from './GraphQLCache'; + import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config'; import type { Logger } from 'vscode-languageserver'; @@ -223,30 +224,31 @@ export class GraphQLLanguageService { return []; } const schema = await this._graphQLCache.getSchema(projectConfig.name); - const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( - projectConfig, - ); + if (!schema) { + return []; + } + let fragmentInfo = [] as Array; + try { + const fragmentDefinitions = + await this._graphQLCache.getFragmentDefinitions(projectConfig); + fragmentInfo = Array.from(fragmentDefinitions).map( + ([, info]) => info.definition, + ); + } catch {} - const fragmentInfo = Array.from(fragmentDefinitions).map( - ([, info]) => info.definition, + return getAutocompleteSuggestions( + schema, + query, + position, + undefined, + fragmentInfo, + { + uri: filePath, + fillLeafsOnComplete: + projectConfig?.extensions?.languageService?.fillLeafsOnComplete ?? + false, + }, ); - - if (schema) { - return getAutocompleteSuggestions( - schema, - query, - position, - undefined, - fragmentInfo, - { - uri: filePath, - fillLeafsOnComplete: - projectConfig?.extensions?.languageService?.fillLeafsOnComplete ?? - false, - }, - ); - } - return []; } public async getHoverInformation( diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 48f67a26afa..291d575f73d 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -474,20 +474,23 @@ export class MessageProcessor { 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), + try { + 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), + ); + } + // skip diagnostic errors, usually related to parsing incomplete fragments + }), + ); + } catch {} } this._logger.log( @@ -600,6 +603,7 @@ export class MessageProcessor { if (range) { position.line -= range.start.line; } + const result = await this._languageService.getAutocompleteSuggestions( query, toPosition(position), @@ -729,9 +733,8 @@ export class MessageProcessor { return { uri, diagnostics }; } - } catch (err) { - this._handleConfigError({ err, uri }); - } + // skip diagnostics errors usually from incomplete files + } catch {} return { uri, diagnostics: [] }; } if (change.type === FileChangeTypeKind.Deleted) { @@ -1191,7 +1194,13 @@ export class MessageProcessor { const schemaFilePath = path.resolve(project.dirpath, schema); const uriFilePath = URI.parse(uri).fsPath; if (uriFilePath === schemaFilePath) { - await this._graphQLCache.invalidateSchemaCacheForProject(project); + try { + const file = await readFile(schemaFilePath, 'utf-8'); + // only invalidate the schema cache if we can actually parse the file + // otherwise, leave the last valid one in place + parse(file, { noLocation: true }); + this._graphQLCache.invalidateSchemaCacheForProject(project); + } catch {} } }), ); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 85f19a20a4f..ffa97eb78e2 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -483,38 +483,6 @@ describe('project with simple config and graphql files', () => { '__schema', '__type', ]); - - // project.changeFile( - // 'b/schema.graphql', - // schemaFile[1] + '\ntype Example1 { field: }', - // ); - // TODO: this didn't work at all for multi project, - // whereas a schema change works above in a single schema context as per updating the cache - // - // how to register incomplete changes to model autocomplete, etc? - // await project.lsp.handleWatchedFilesChangedNotification({ - // changes: [ - // { uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed }, - // ], - // }); - // better - fails on a graphql parsing error! annoying - // await project.lsp.handleDidChangeNotification({ - // textDocument: { uri: project.uri('b/schema.graphql'), version: 1 }, - // contentChanges: [ - // { text: schemaFile[1] + '\ntype Example1 { field: }' }, - // ], - // }); - - // const schemaCompletion = await project.lsp.handleCompletionRequest({ - // textDocument: { uri: project.uri('b/schema.graphql') }, - // position: { character: 23, line: 3 }, - // }); - // expect(schemaCompletion.items.map(i => i.label)).toEqual([ - // 'foo', - // '__typename', - // '__schema', - // '__type', - // ]); // this confirms that autocomplete respects cross-project boundaries for types. // it performs a definition request for the foo field in Query const schemaCompletion1 = await project.lsp.handleCompletionRequest({ @@ -529,6 +497,39 @@ describe('project with simple config and graphql files', () => { }); expect(serializeRange(schemaDefinition[0].range)).toEqual(fooTypePosition); + // simulate a watched schema file change (codegen, etc) + project.changeFile( + 'b/schema.graphql', + schemaFile[1] + '\ntype Example1 { field: }', + ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('b/schema.graphql'), type: FileChangeType.Changed }, + ], + }); + // TODO: repeat this with other changes to the schema file and use a + // didChange event to see if the schema updates properly as well + // await project.lsp.handleDidChangeNotification({ + // textDocument: { uri: project.uri('b/schema.graphql'), version: 1 }, + // contentChanges: [ + // { text: schemaFile[1] + '\ntype Example1 { field: }' }, + // ], + // }); + // console.log(project.fileCache.get('b/schema.graphql')); + const schemaCompletion = await project.lsp.handleCompletionRequest({ + textDocument: { uri: project.uri('b/schema.graphql') }, + position: { character: 25, line: 5 }, + }); + // TODO: SDL completion still feels incomplete here... where is Int? + // where is self-referential Example1? + expect(schemaCompletion.items.map(i => i.label)).toEqual([ + 'Query', + 'Foo', + 'String', + 'Test', + 'Boolean', + ]); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); }); diff --git a/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts b/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts index f7daabfe2f5..3efe44c0422 100644 --- a/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts +++ b/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts @@ -607,6 +607,39 @@ describe('getAutocompleteSuggestions', () => { { label: 'TestType' }, { label: 'TestUnion' }, ])); + // TODO: shouldn't TestType and TestUnion be available here? + it('provides correct filtered suggestions on object fields in regular SDL files', () => + expect( + testSuggestions('type Type {\n aField: s', new Position(0, 23), [], { + uri: 'schema.graphql', + }), + ).toEqual([ + { label: 'Episode' }, + { label: 'String' }, + { label: 'TestInterface' }, + { label: 'TestType' }, + { label: 'TestUnion' }, + ])); + it('provides correct unfiltered suggestions on object fields in regular SDL files', () => + expect( + testSuggestions('type Type {\n aField: ', new Position(0, 22), [], { + uri: 'schema.graphql', + }), + ).toEqual([ + { label: 'AnotherInterface' }, + { label: 'Boolean' }, + { label: 'Character' }, + { label: 'Droid' }, + { label: 'Episode' }, + { label: 'Human' }, + { label: 'Int' }, + // TODO: maybe filter out types attached to top level schema? + { label: 'Query' }, + { label: 'String' }, + { label: 'TestInterface' }, + { label: 'TestType' }, + { label: 'TestUnion' }, + ])); it('provides correct suggestions on object fields that are arrays', () => expect( testSuggestions('type Type {\n aField: []', new Position(0, 25), [], { @@ -626,6 +659,25 @@ describe('getAutocompleteSuggestions', () => { { label: 'TestType' }, { label: 'TestUnion' }, ])); + it('provides correct suggestions on object fields that are arrays in SDL context', () => + expect( + testSuggestions('type Type {\n aField: []', new Position(0, 25), [], { + uri: 'schema.graphql', + }), + ).toEqual([ + { label: 'AnotherInterface' }, + { label: 'Boolean' }, + { label: 'Character' }, + { label: 'Droid' }, + { label: 'Episode' }, + { label: 'Human' }, + { label: 'Int' }, + { label: 'Query' }, + { label: 'String' }, + { label: 'TestInterface' }, + { label: 'TestType' }, + { label: 'TestUnion' }, + ])); it('provides correct suggestions on input object fields', () => expect( testSuggestions('input Type {\n aField: s', new Position(0, 23), [], { diff --git a/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts b/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts index 5c01896a22d..e5b5a13c8f1 100644 --- a/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts +++ b/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts @@ -399,34 +399,27 @@ export function getAutocompleteSuggestions( const unwrappedState = unwrapType(state); - if ( - (mode === GraphQLDocumentMode.TYPE_SYSTEM && - !unwrappedState.needsAdvance && - kind === RuleKinds.NAMED_TYPE) || - kind === RuleKinds.LIST_TYPE - ) { - if (unwrappedState.kind === RuleKinds.FIELD_DEF) { - return hintList( - token, - Object.values(schema.getTypeMap()) - .filter(type => isOutputType(type) && !type.name.startsWith('__')) - .map(type => ({ - label: type.name, - kind: CompletionItemKind.Function, - })), - ); - } - if (unwrappedState.kind === RuleKinds.INPUT_VALUE_DEF) { - return hintList( - token, - Object.values(schema.getTypeMap()) - .filter(type => isInputType(type) && !type.name.startsWith('__')) - .map(type => ({ - label: type.name, - kind: CompletionItemKind.Function, - })), - ); - } + if (unwrappedState.kind === RuleKinds.FIELD_DEF) { + return hintList( + token, + Object.values(schema.getTypeMap()) + .filter(type => isOutputType(type) && !type.name.startsWith('__')) + .map(type => ({ + label: type.name, + kind: CompletionItemKind.Function, + })), + ); + } + if (unwrappedState.kind === RuleKinds.INPUT_VALUE_DEF) { + return hintList( + token, + Object.values(schema.getTypeMap()) + .filter(type => isInputType(type) && !type.name.startsWith('__')) + .map(type => ({ + label: type.name, + kind: CompletionItemKind.Function, + })), + ); } // Variable definition types From c1053fd5b9c34ad0f3499bf5697db2bdd0c4ec8a Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 3 Mar 2024 20:33:52 +0100 Subject: [PATCH 41/49] fix log level, keep things relevant --- .../src/Logger.ts | 47 +++++++++++- .../src/MessageProcessor.ts | 26 +++++-- .../src/__tests__/Logger.test.ts | 73 +++++++++++++++++++ .../src/parsers/astro.ts | 14 ++-- .../src/parsers/babel.ts | 24 +++--- .../src/parsers/svelte.ts | 12 +-- .../src/parsers/vue.ts | 14 ++-- .../src/startServer.ts | 10 ++- 8 files changed, 179 insertions(+), 41 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/Logger.test.ts diff --git a/packages/graphql-language-service-server/src/Logger.ts b/packages/graphql-language-service-server/src/Logger.ts index ccc58defa81..69f8a621268 100644 --- a/packages/graphql-language-service-server/src/Logger.ts +++ b/packages/graphql-language-service-server/src/Logger.ts @@ -11,7 +11,48 @@ import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import { Connection } from 'vscode-languageserver'; export class Logger implements VSCodeLogger { - constructor(private _connection: Connection) {} + // TODO: allow specifying exact log level? + // for now this is to handle the debug setting + private logLevel: number; + constructor( + private _connection: Connection, + debug?: boolean, + ) { + this.logLevel = debug ? 1 : 0; + // first detect the debug flag on initialization + void (async () => { + try { + const config = await this._connection?.workspace?.getConfiguration( + 'vscode-graphql', + ); + const debugSetting = config?.get('debug'); + if (debugSetting === true) { + this.logLevel = 1; + } + if (debugSetting === false || debugSetting === null) { + this.logLevel = 0; + } + } catch { + // ignore + } + })(); + // then watch for it to change. doesn't require re-creating the logger! + this._connection?.onDidChangeConfiguration(config => { + const debugSetting = + config?.settings && config.settings['vscode-graphql']?.debug; + // if it's undefined, it's not being passed + if (debugSetting === undefined) { + return; + } + // if it's true, set it to 1, we will eventually do log levels properly + if (debugSetting === true) { + this.logLevel = 1; + } + if (debugSetting === false || debugSetting === null) { + this.logLevel = 0; + } + }); + } error(message: string): void { this._connection.console.error(message); @@ -26,7 +67,9 @@ export class Logger implements VSCodeLogger { } log(message: string): void { - this._connection.console.log(message); + if (this.logLevel > 0) { + this._connection.console.log(message); + } } } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 291d575f73d..4b37583d41d 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -255,8 +255,14 @@ export class MessageProcessor { const config = this._graphQLCache.getGraphQLConfig(); if (config) { await this._cacheAllProjectFiles(config); + // TODO: per project lazy instantiation. + // we had it working before, but it seemed like it caused bugs + // which were caused by something else. + // thus. _isInitialized should be replaced with something like + // projectHasInitialized: (projectName: string) => boolean this._isInitialized = true; this._isGraphQLConfigMissing = false; + this._logger.info('GraphQL Language Server caches initialized'); } } catch (err) { this._handleConfigError({ err }); @@ -387,9 +393,6 @@ export class MessageProcessor { // i think this is because if the file change is empty, it doesn't get parsed // TODO: this could be related to a bug in how we are calling didOpenOrSave in our tests // that doesn't reflect the real runtime behavior - // if (!text || text.length < 1) { - // return { uri, diagnostics }; - // } const { contents } = await this._parseAndCacheFile( uri, @@ -963,8 +966,8 @@ export class MessageProcessor { await this._invalidateCache({ version, uri }, uri, contents); await this._updateObjectTypeDefinition(uri, contents, project); } - } catch { - // this._logger.error(String(err)); + } catch (err) { + this._logger.error(String(err)); } } private async _cacheSchemaFile( @@ -1158,8 +1161,19 @@ export class MessageProcessor { return Promise.all( Object.keys(config.projects).map(async projectName => { const project = config.getProject(projectName); + await this._cacheSchemaFilesForProject(project); - await this._cacheDocumentFilesforProject(project); + if (project.documents?.length) { + await this._cacheDocumentFilesforProject(project); + } else { + this._logger.warn( + [ + `No 'documents' config found for project: ${projectName}.`, + 'Fragments and query documents cannot be detected.', + 'LSP server will only perform some partial validation and SDL features.', + ].join('\n'), + ); + } }), ); } diff --git a/packages/graphql-language-service-server/src/__tests__/Logger.test.ts b/packages/graphql-language-service-server/src/__tests__/Logger.test.ts new file mode 100644 index 00000000000..a477947ddbc --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/Logger.test.ts @@ -0,0 +1,73 @@ +import { Logger } from '../Logger'; + +describe('Logger', () => { + const connection = { + console: { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), + }, + onDidChangeConfiguration: jest.fn(), + workspace: { + getConfiguration: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize with default log level, and ignore .log intentionally', () => { + const logger = new Logger(connection as any); + expect(logger).toBeDefined(); + expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); + expect(logger.logLevel).toBe(0); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(0); + }); + + it('should initialize with default log level, then change to logging with new settings, then back when they are disabled', () => { + const logger = new Logger(connection as any); + expect(logger).toBeDefined(); + expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); + expect(logger.logLevel).toBe(0); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(0); + connection.onDidChangeConfiguration.mock.calls[0][0]({ + settings: { 'vscode-graphql': { debug: true } }, + }); + expect(logger.logLevel).toBe(1); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(1); + connection.onDidChangeConfiguration.mock.calls[0][0]({ + settings: { 'vscode-graphql': { debug: false } }, + }); + expect(logger.logLevel).toBe(0); + logger.log('test'); + // and not a second time + expect(connection.console.log).toHaveBeenCalledTimes(1); + }); + + it('should not change log level when settings are not passed', () => { + const logger = new Logger(connection as any, true); + expect(logger).toBeDefined(); + expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); + expect(logger.logLevel).toBe(1); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(1); + connection.onDidChangeConfiguration.mock.calls[0][0]({}); + expect(logger.logLevel).toBe(1); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(2); + }); + + it('should initialize with debug log level, and .log is visible now', () => { + const logger = new Logger(connection as any, true); + expect(logger).toBeDefined(); + expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); + expect(logger.logLevel).toBe(1); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/graphql-language-service-server/src/parsers/astro.ts b/packages/graphql-language-service-server/src/parsers/astro.ts index b09a315d11f..d0559fabb8b 100644 --- a/packages/graphql-language-service-server/src/parsers/astro.ts +++ b/packages/graphql-language-service-server/src/parsers/astro.ts @@ -41,15 +41,15 @@ function parseAstro(source: string): ParseAstroResult { return { type: 'error', errors: ['Could not find frontmatter block'] }; } -export const astroParser: SourceParser = (text, _uri, _logger) => { +export const astroParser: SourceParser = (text, uri, logger) => { const parseAstroResult = parseAstro(text); if (parseAstroResult.type === 'error') { - // logger.error( - // `Could not parse the astro file at ${uri} to extract the graphql tags:`, - // ); - // for (const error of parseAstroResult.errors) { - // logger.error(String(error)); - // } + logger.info( + `Could not parse the astro file at ${uri} to extract the graphql tags:`, + ); + for (const error of parseAstroResult.errors) { + logger.info(String(error)); + } return null; } diff --git a/packages/graphql-language-service-server/src/parsers/babel.ts b/packages/graphql-language-service-server/src/parsers/babel.ts index 05887352fd7..4216c11a50e 100644 --- a/packages/graphql-language-service-server/src/parsers/babel.ts +++ b/packages/graphql-language-service-server/src/parsers/babel.ts @@ -11,26 +11,26 @@ export const babelParser = (text: string, plugins?: ParserPlugin[]) => { return parse(text, PARSER_OPTIONS); }; -export const ecmaParser: SourceParser = (text, _uri, _logger) => { +export const ecmaParser: SourceParser = (text, uri, logger) => { try { return { asts: [babelParser(text, ['flow', 'flowComments'])] }; - } catch { - // logger.error( - // `Could not parse the JavaScript file at ${uri} to extract the graphql tags:`, - // ); - // logger.error(String(error)); + } catch (error) { + logger.info( + `Could not parse the JavaScript file at ${uri} to extract the graphql tags:`, + ); + logger.info(String(error)); return null; } }; -export const tsParser: SourceParser = (text, _uri, _logger) => { +export const tsParser: SourceParser = (text, uri, logger) => { try { return { asts: [babelParser(text, ['typescript'])] }; - } catch { - // logger.error( - // `Could not parse the TypeScript file at ${uri} to extract the graphql tags:`, - // ); - // logger.error(String(error)); + } catch (error) { + logger.info( + `Could not parse the TypeScript file at ${uri} to extract the graphql tags:`, + ); + logger.info(String(error)); return null; } }; diff --git a/packages/graphql-language-service-server/src/parsers/svelte.ts b/packages/graphql-language-service-server/src/parsers/svelte.ts index 4a1e9b9c3eb..e838271ff29 100644 --- a/packages/graphql-language-service-server/src/parsers/svelte.ts +++ b/packages/graphql-language-service-server/src/parsers/svelte.ts @@ -4,7 +4,7 @@ import { SourceMapConsumer } from 'source-map-js'; import { Position, Range } from 'graphql-language-service'; import type { RangeMapper, SourceParser } from './types'; -export const svelteParser: SourceParser = (text, uri, _logger) => { +export const svelteParser: SourceParser = (text, uri, logger) => { const svelteResult = svelte2tsx(text, { filename: uri, }); @@ -35,11 +35,11 @@ export const svelteParser: SourceParser = (text, uri, _logger) => { asts: [babelParser(svelteResult.code, ['typescript'])], rangeMapper, }; - } catch { - // logger.error( - // `Could not parse the Svelte file at ${uri} to extract the graphql tags:`, - // ); - // logger.error(String(error)); + } catch (error) { + logger.info( + `Could not parse the Svelte file at ${uri} to extract the graphql tags:`, + ); + logger.info(String(error)); return null; } }; diff --git a/packages/graphql-language-service-server/src/parsers/vue.ts b/packages/graphql-language-service-server/src/parsers/vue.ts index b982012c2db..a1a80be2d52 100644 --- a/packages/graphql-language-service-server/src/parsers/vue.ts +++ b/packages/graphql-language-service-server/src/parsers/vue.ts @@ -45,16 +45,16 @@ export function parseVueSFC(source: string): ParseVueSFCResult { }; } -export const vueParser: SourceParser = (text, _uri, _logger) => { +export const vueParser: SourceParser = (text, uri, logger) => { const asts = []; const parseVueSFCResult = parseVueSFC(text); if (parseVueSFCResult.type === 'error') { - // logger.error( - // `Could not parse the vue file at ${uri} to extract the graphql tags:`, - // ); - // for (const error of parseVueSFCResult.errors) { - // logger.error(String(error)); - // } + logger.info( + `Could not parse the vue file at ${uri} to extract the graphql tags:`, + ); + for (const error of parseVueSFCResult.errors) { + logger.info(String(error)); + } return null; } diff --git a/packages/graphql-language-service-server/src/startServer.ts b/packages/graphql-language-service-server/src/startServer.ts index 69924e2ac98..e22c3a8df79 100644 --- a/packages/graphql-language-service-server/src/startServer.ts +++ b/packages/graphql-language-service-server/src/startServer.ts @@ -102,6 +102,14 @@ export interface ServerOptions { * the temporary directory that the server writes to for logs and caching schema */ tmpDir?: string; + + /** + * debug mode + * + * same as with the client reference implementation, the debug setting controls logging output + * this allows all logger.info() messages to come through. by default, the highest level is warn + */ + debug?: true; } /** @@ -217,7 +225,7 @@ async function initializeHandlers({ options, }: InitializerParams): Promise { const connection = createConnection(reader, writer); - const logger = new Logger(connection); + const logger = new Logger(connection, options.debug); try { await addHandlers({ connection, logger, ...options }); From 0036c7f652b15602a4492af9340e35c57aa6b348 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 3 Mar 2024 21:56:27 +0100 Subject: [PATCH 42/49] fix: logger tests, simple re-instantiation on settings change --- .../src/Logger.ts | 43 +++++-------------- .../src/MessageProcessor.ts | 2 + .../src/__tests__/Logger.test.ts | 34 --------------- .../src/__tests__/startServer.spec.ts | 9 ++++ 4 files changed, 21 insertions(+), 67 deletions(-) create mode 100644 packages/graphql-language-service-server/src/__tests__/startServer.spec.ts diff --git a/packages/graphql-language-service-server/src/Logger.ts b/packages/graphql-language-service-server/src/Logger.ts index 69f8a621268..85f530f1fd0 100644 --- a/packages/graphql-language-service-server/src/Logger.ts +++ b/packages/graphql-language-service-server/src/Logger.ts @@ -19,39 +19,6 @@ export class Logger implements VSCodeLogger { debug?: boolean, ) { this.logLevel = debug ? 1 : 0; - // first detect the debug flag on initialization - void (async () => { - try { - const config = await this._connection?.workspace?.getConfiguration( - 'vscode-graphql', - ); - const debugSetting = config?.get('debug'); - if (debugSetting === true) { - this.logLevel = 1; - } - if (debugSetting === false || debugSetting === null) { - this.logLevel = 0; - } - } catch { - // ignore - } - })(); - // then watch for it to change. doesn't require re-creating the logger! - this._connection?.onDidChangeConfiguration(config => { - const debugSetting = - config?.settings && config.settings['vscode-graphql']?.debug; - // if it's undefined, it's not being passed - if (debugSetting === undefined) { - return; - } - // if it's true, set it to 1, we will eventually do log levels properly - if (debugSetting === true) { - this.logLevel = 1; - } - if (debugSetting === false || debugSetting === null) { - this.logLevel = 0; - } - }); } error(message: string): void { @@ -71,6 +38,12 @@ export class Logger implements VSCodeLogger { this._connection.console.log(message); } } + set level(level: number) { + this.logLevel = level; + } + get level() { + return this.logLevel; + } } export class NoopLogger implements VSCodeLogger { @@ -78,4 +51,8 @@ export class NoopLogger implements VSCodeLogger { warn() {} info() {} log() {} + set level(_level: number) {} + get level() { + return 0; + } } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 4b37583d41d..cb3a2c2c2dd 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -225,6 +225,8 @@ export class MessageProcessor { }; try { + // now we have the settings so we can re-build the logger + this._logger.level = this._settings?.debug === true ? 1 : 0; // createServer() can be called with a custom config object, and // this is a public interface that may be used by customized versions of the server if (this._providedConfig) { diff --git a/packages/graphql-language-service-server/src/__tests__/Logger.test.ts b/packages/graphql-language-service-server/src/__tests__/Logger.test.ts index a477947ddbc..82ac05fd097 100644 --- a/packages/graphql-language-service-server/src/__tests__/Logger.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/Logger.test.ts @@ -21,53 +21,19 @@ describe('Logger', () => { it('should initialize with default log level, and ignore .log intentionally', () => { const logger = new Logger(connection as any); expect(logger).toBeDefined(); - expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); expect(logger.logLevel).toBe(0); logger.log('test'); expect(connection.console.log).toHaveBeenCalledTimes(0); }); - it('should initialize with default log level, then change to logging with new settings, then back when they are disabled', () => { - const logger = new Logger(connection as any); - expect(logger).toBeDefined(); - expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); - expect(logger.logLevel).toBe(0); - logger.log('test'); - expect(connection.console.log).toHaveBeenCalledTimes(0); - connection.onDidChangeConfiguration.mock.calls[0][0]({ - settings: { 'vscode-graphql': { debug: true } }, - }); - expect(logger.logLevel).toBe(1); - logger.log('test'); - expect(connection.console.log).toHaveBeenCalledTimes(1); - connection.onDidChangeConfiguration.mock.calls[0][0]({ - settings: { 'vscode-graphql': { debug: false } }, - }); - expect(logger.logLevel).toBe(0); - logger.log('test'); - // and not a second time - expect(connection.console.log).toHaveBeenCalledTimes(1); - }); - it('should not change log level when settings are not passed', () => { const logger = new Logger(connection as any, true); expect(logger).toBeDefined(); - expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); expect(logger.logLevel).toBe(1); logger.log('test'); expect(connection.console.log).toHaveBeenCalledTimes(1); - connection.onDidChangeConfiguration.mock.calls[0][0]({}); expect(logger.logLevel).toBe(1); logger.log('test'); expect(connection.console.log).toHaveBeenCalledTimes(2); }); - - it('should initialize with debug log level, and .log is visible now', () => { - const logger = new Logger(connection as any, true); - expect(logger).toBeDefined(); - expect(connection.onDidChangeConfiguration).toHaveBeenCalledTimes(1); - expect(logger.logLevel).toBe(1); - logger.log('test'); - expect(connection.console.log).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/graphql-language-service-server/src/__tests__/startServer.spec.ts b/packages/graphql-language-service-server/src/__tests__/startServer.spec.ts new file mode 100644 index 00000000000..dbf4a496f7b --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/startServer.spec.ts @@ -0,0 +1,9 @@ +import startServer from '../startServer'; + +describe('startServer', () => { + it('should start the server', async () => { + await startServer({}); + // if the server starts, we're good + expect(true).toBe(true); + }); +}); From 2d1cbbbb3e0a484b41ec45fe3e4d2787a747c73f Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 3 Mar 2024 22:26:17 +0100 Subject: [PATCH 43/49] add changeset --- .changeset/rotten-seahorses-fry.md | 35 +++++++++++++++++++++++++++--- .changeset/silly-yaks-bathe.md | 11 ++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 .changeset/silly-yaks-bathe.md diff --git a/.changeset/rotten-seahorses-fry.md b/.changeset/rotten-seahorses-fry.md index e30441acd17..b1635cf4d52 100644 --- a/.changeset/rotten-seahorses-fry.md +++ b/.changeset/rotten-seahorses-fry.md @@ -1,6 +1,35 @@ --- -'graphql-language-service-server': patch -'vscode-graphql-syntax': patch +'graphql-language-service-server': minor +'vscode-graphql': minor +'graphql-language-service-server-cli': minor --- -Fix crash on saving empty package.json file +Fix many schema and fragment lifecycle issues, for all contexts except for schema updates for url schemas. +Note: this makes `cacheSchemaForLookup` enabled by default again for schema first contexts. + +this fixes multiple cacheing bugs, on writing some in-depth integration coverage for the LSP server. +it also solves several bugs regarding loading config types, and properly restarts the server when there are config changes + +### Bugfix Summary + +- jump to definition in embedded files offset bug +- cache invalidation for fragments +- schema cache invalidation for schema files +- schema definition lookups & autocomplete crossing into the wrong workspace + +### Known Bugs Fixed + +- #3318 +- #2357 +- #3469 +- #2422 +- #2820 +- many others to add here... + +### Test Improvements + +- new, high level integration spec suite for the LSP with a matching test utility +- more unit test coverage +- **total increased test coverage of about 25% in the LSP server codebase.** +- many "happy paths" covered for both schema and code first contexts +- many bugs revealed (and their source) diff --git a/.changeset/silly-yaks-bathe.md b/.changeset/silly-yaks-bathe.md new file mode 100644 index 00000000000..b7f2839f4e7 --- /dev/null +++ b/.changeset/silly-yaks-bathe.md @@ -0,0 +1,11 @@ +--- +'graphiql': patch +'graphql-language-service': patch +'graphql-language-service-server': patch +'graphql-language-service-server-cli': patch +'codemirror-graphql': patch +'@graphiql/react': patch +'monaco-graphql': patch +--- + +bugfix to completion for SDL type fields From 479203c2d6e285457d8f55d12af8b3a15c83b720 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 8 Mar 2024 21:59:28 +0100 Subject: [PATCH 44/49] fix: env, timeout --- .../src/MessageProcessor.ts | 9 ++++++--- .../src/__tests__/MessageProcessor.test.ts | 2 ++ packages/vscode-graphql/package.json | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index cb3a2c2c2dd..e97b5a6f2ee 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -203,15 +203,18 @@ export class MessageProcessor { const vscodeSettings = await this._connection.workspace.getConfiguration({ section: 'vscode-graphql', }); - if (settings?.dotEnvPath) { - require('dotenv').config({ path: settings.dotEnvPath }); - } + // TODO: eventually we will instantiate an instance of this per workspace, // so rootDir should become that workspace's rootDir this._settings = { ...settings, ...vscodeSettings }; const rootDir = this._settings?.load?.rootDir.length ? this._settings?.load?.rootDir : this._rootPath; + if (settings?.dotEnvPath) { + require('dotenv').config({ + path: path.resolve(rootDir, settings.dotEnvPath), + }); + } this._rootPath = rootDir; this._loadConfigOptions = { ...Object.keys(this._settings?.load ?? {}).reduce((agg, key) => { 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 e58b5979f01..b22af3ff772 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -18,6 +18,8 @@ import { parseDocument } from '../parseDocument'; jest.mock('../Logger'); +jest.setTimeout(20000); + import { GraphQLCache } from '../GraphQLCache'; import { diff --git a/packages/vscode-graphql/package.json b/packages/vscode-graphql/package.json index 767b58a127f..4af120ef066 100644 --- a/packages/vscode-graphql/package.json +++ b/packages/vscode-graphql/package.json @@ -1,6 +1,6 @@ { "name": "vscode-graphql", - "version": "0.9.3", + "version": "0.10.1", "private": true, "license": "MIT", "displayName": "GraphQL: Language Feature Support", @@ -161,7 +161,7 @@ "vsce:package": "vsce package --yarn", "env:source": "export $(cat .envrc | xargs)", "vsce:publish": "vsce publish --yarn", - "open-vsx:publish": "ovsx publish --extensionFile $(ls -1 *.vsix | tail -n 1) --pat $OVSX_PAT", + "open-vsx:publish": "ovsx publish $(ls -1 *.vsix | tail -n 1) --pat $OVSX_PAT", "release": "npm run vsce:publish && npm run open-vsx:publish" }, "devDependencies": { From 61d91a795ac48afdd19520c531cb1a31335eae6b Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 17 Mar 2024 10:44:45 +0100 Subject: [PATCH 45/49] docs: pluck some docs improvements from the next phase --- .../graphql-language-service-server/README.md | 18 +- packages/vscode-graphql/README.md | 228 +++++------------- packages/vscode-graphql/package.json | 30 +-- 3 files changed, 82 insertions(+), 194 deletions(-) diff --git a/packages/graphql-language-service-server/README.md b/packages/graphql-language-service-server/README.md index d0f5388cd9a..9022aaecea8 100644 --- a/packages/graphql-language-service-server/README.md +++ b/packages/graphql-language-service-server/README.md @@ -157,7 +157,7 @@ module.exports = { // 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 definition lookups from a cached file based on graphql config output? + // this is enabled by default if non-local files are specified in the project `schema` // 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" @@ -237,14 +237,14 @@ 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` | `true` if `schema` contains non-sdl files or urls | generate an SDL file based on your graphql-config schema configuration for schema definition lookup and other features. enabled by default when your `schema` config are urls or introspection json, or if you have any non-local SDL files in `schema` | all the `graphql-config.load.*` configuration values come from static `loadConfig()` options in graphql config. diff --git a/packages/vscode-graphql/README.md b/packages/vscode-graphql/README.md index 6cad22385da..9a828269d22 100644 --- a/packages/vscode-graphql/README.md +++ b/packages/vscode-graphql/README.md @@ -7,28 +7,9 @@ Ecosystem with VSCode for an awesome developer experience. ![](https://camo.githubusercontent.com/97dc1080d5e6883c4eec3eaa6b7d0f29802e6b4b/687474703a2f2f672e7265636f726469742e636f2f497379504655484e5a342e676966) -### General features - -> _Operation Execution will be re-introduced in a new extension_ - -- Load the extension on detecting `graphql-config file` at root level or in a - parent level directory -- Load the extension in `.graphql`, `.gql files` -- Load the extension detecting `gql` tag in js, ts, jsx, tsx, vue files -- Load the extension inside `gql`/`graphql` fenced code blocks in markdown files -- NO LONGER SUPPORTED - execute query/mutation/subscription operations, embedded - or in graphql files - we will be recommending other extensions for this. -- pre-load schema and document definitions -- Support [`graphql-config`](https://graphql-config.com/) files with one project - and multiple projects (multi-workspace roots with multiple graphql config - files not yet supported) -- the language service re-starts on saved changes to vscode settings and/or - graphql config! - -### `.graphql`, `.gql` file extension support +### `.graphql`, `.gql` file extension support and `gql`/`graphql` tagged template literal support for tsx, jsx, ts, js -- syntax highlighting (type, query, mutation, interface, union, enum, scalar, - fragments, directives) +- syntax highlighting (provided by `vscode-graphql-syntax`) - autocomplete suggestions - validation against schema - snippets (interface, type, input, enum, union) @@ -36,80 +17,67 @@ Ecosystem with VSCode for an awesome developer experience. - go to definition support (input, enum, type) - outline support -### `gql`/`graphql` tagged template literal support for tsx, jsx, ts, js +## Getting Started -- syntax highlighting (type, query, mutation, interface, union, enum, scalar, - fragments, directives) -- autocomplete suggestions -- validation against schema -- snippets -- hover support -- go to definition for fragments and input types -- outline support +> **This extension requires a graphql-config file**. -## Usage +To support language features like completion and "go-to definition" across multiple files, +please include `documents` in the `graphql-config` file default or per-project -**This extension requires a graphql-config file**. +### Simplest Config Example -Install the -[VSCode GraphQL Extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql). - -(Watchman is no longer required, you can uninstall it now) +```yaml +# .graphqlrc.yml or graphql.config.yml +schema: 'schema.graphql' +documents: 'src/**/*.{graphql,js,ts,jsx,tsx}' +``` -As of `vscode-graphql@0.3.0` we support `graphql-config@3`. You can read more -about that [here](https://www.graphql-config.com/docs/user/user-usage). Because -it now uses `cosmiconfig` there are plenty of new options for loading config -files: +`package.json`: +```json +"graphql": { + "schema": "https://localhost:3001", + "documents": "**/*.{graphql,js,ts,jsx,tsx}" +}, ``` -graphql.config.json -graphql.config.js -graphql.config.yaml -graphql.config.yml -.graphqlrc (YAML or JSON) -.graphqlrc.json -.graphqlrc.yaml -.graphqlrc.yml -.graphqlrc.js -graphql property in package.json -``` - -the file needs to be placed at the project root by default, but you can -configure paths per project. see the FAQ below for details. -Previous versions of this extension support `graphql-config@2` format, which -follows -[legacy configuration patterns](https://github.com/kamilkisiela/graphql-config/tree/legacy#usage) +```ts +// .graphqlrc.ts or graphql.config.ts +export default { + schema: 'schema.graphql', + documents: '**/*.{graphql,js,ts,jsx,tsx}', +}; +``` -If you need legacy support for `.graphqlconfig` files or older graphql-config -formats, see [this FAQ answer](#legacy). If you are missing legacy -`graphql-config` features, please consult -[the `graphql-config` repository](https://github.com/kamilkisiela/graphql-config). +## Additional Features -To support language features like "go-to definition" across multiple files, -please include `documents` key in the `graphql-config` file default or -per-project (this was `include` in 2.0). +- Loads the LSP server upon detecting a `graphql-config` file at root level or in a + parent level directory, or a `package.json` file with `graphql` config +- Loads `.graphql`, `.gql` files, and detects `gql`, `graphql` tags and `/** GraphQL */` and `#graphql` comments in js, ts, jsx, tsx, vue files +- pre-load schema and fragment definitions +- Support [`graphql-config`](https://graphql-config.com/) files with one project + and multiple projects (multi-workspace roots with multiple graphql config + files not yet supported) +- the language service re-starts on saved changes to vscode settings and/or + graphql config! ## Configuration Examples For more help with configuring the language server, [the language server readme](https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-server/README.md) is the source of truth for all settings used by all editors which use the -language server. +language server. The [`graphql-config`](https://graphql-config.com/) docs are also very helpful. -### Simple Example +### Advanced Example -```yaml -# .graphqlrc.yml -schema: 'schema.graphql' -documents: 'src/**/*.{graphql,js,ts,jsx,tsx}' -``` +Multi-project can be used for both local files, URL defined schema, or both -### Advanced Example +```ts +import dotenv from 'dotenv'; +dotenv.config(); -```js -// graphql.config.js -module.exports = { +// .graphqlrc.ts or graphql.config.ts +export default { projects: { app: { schema: ['src/schema.graphql', 'directives.graphql'], @@ -119,15 +87,15 @@ module.exports = { schema: 'src/generated/db.graphql', documents: ['src/db/**/*.graphql', 'my/fragments.graphql'], extensions: { - codegen: [ - { - generator: 'graphql-binding', - language: 'typescript', - output: { - binding: 'src/generated/db.ts', + // for use with `vscode-graphql-execution`, for example: + endpoints: { + default: { + url: 'https://localhost:3001/graphql/', + headers: { + Authorization: `Bearer ${process.env.API_TOKEN}`, }, }, - ], + }, }, }, }, @@ -139,66 +107,9 @@ is also valid. ## Frequently Asked Questions - - -### I can't load `.graphqlconfig` files anymore - -> Note: this option has been set to be enabled by default, however -> `graphql-config` maintainers do not want to continue to support the legacy -> format (mostly kept for companies where intellij users are stuck on the old -> config format), so please migrate to the new `graphql-config` format as soon -> as possible! - -If you need to use a legacy config file, then you just need to enable legacy -mode for `graphql-config`: - -```json -"graphql-config.load.legacy": true -``` - -### Go to definition is not working for my URL - -You can try the new experimental `cacheSchemaFileForLookup` option. NOTE: this -will disable all definition lookup for local SDL graphql schema files, and -_only_ perform lookup of the result an SDL result of `graphql-config` -`getSchema()` - -To enable, add this to your settings: - -```json -"vscode-graphql.cacheSchemaFileForLookup": true, -``` - -you can also use graphql config if you need to mix and match these settings: - -```yml -schema: 'http://myschema.com/graphql' -extensions: - languageService: - cacheSchemaFileForLookup: true -projects: - project1: - schema: 'project1/schema/schema.graphql' - documents: 'project1/queries/**/*.{graphql,tsx,jsx,ts,js}' - extensions: - languageService: - cacheSchemaFileForLookup: false - - project2: - schema: 'https://api.spacex.land/graphql/' - documents: 'project2/queries.graphql' - extensions: - endpoints: - default: - url: 'https://api.spacex.land/graphql/' - languageService: - # Do project configs inherit parent config? - cacheSchemaFileForLookup: true -``` - ### The extension fails with errors about duplicate types -Make sure that you aren't including schema files in the `documents` blob +Your object types must be unique per project (as they must be unique per schema), and your fragment names must also be unique per project. ### The extension fails with errors about missing scalars, directives, etc @@ -232,6 +143,7 @@ You can search a folder for any of the matching config file names listed above: ```json "graphql-config.load.rootDir": "./config" +"graphql-config.envFilePath": "./config/.dev.env" ``` Or a specific filepath: @@ -253,39 +165,15 @@ which would search for `./config/.acmerc`, `.config/.acmerc.js`, If you have multiple projects, you need to define one top-level config that defines all project configs using `projects` -### How do I highlight an embedded graphql string? - -If you aren't using a template tag function such as `gql` or `graphql`, and just -want to use a plain string, you can use an inline `#graphql` comment: +### How do I enable language features for an embedded graphql string? -```ts -const myQuery = `#graphql - query { - something - } -`; -``` - -or - -```ts -const myQuery = - /* GraphQL */ - - ` - query { - something - } - `; -``` +Please refer to the `vscode-graphql-syntax` reference files ([js](https://github.com/graphql/graphiql/blob/main/packages/vscode-graphql-syntax/tests/__fixtures__/test.js),[ts](https://github.com/graphql/graphiql/blob/main/packages/vscode-graphql-syntax/tests/__fixtures__/test.ts),[svelte](https://github.com/graphql/graphiql/blob/main/packages/vscode-graphql-syntax/tests/__fixtures__/test.svelte),[vue](https://github.com/graphql/graphiql/blob/main/packages/vscode-graphql-syntax/tests/__fixtures__/test.vue)) to learn our template tag, comment and other graphql delimiter patterns for the file types that the language server supports. The syntax highlighter currently supports more languages than the language server. If you notice any places where one or the other doesn't work, please report it! ## Known Issues -- the output channel occasionally shows "definition not found" when you first - start the language service, but once the definition cache is built for each - project, definition lookup will work. so if a "peek definition" fails when you - first start the editor or when you first install the extension, just try the - definition lookup again. +- the locally generated schema file for definition lookup currently does not re-generate on schema changes. this will be fixed soon. +- multi-root workspaces support will be added soon as well. +- some graphql-config options aren't always honored, this will also be fixed soon ## Attribution @@ -312,7 +200,7 @@ This plugin uses the ### Contributing back to this project This repository is managed by EasyCLA. Project participants must sign the free -([GraphQL Specification Membership agreement](https://preview-spec-membership.graphql.org) +([GraphQL Specification Membership agreement](https://preview-spec-membership.graphql.org)) before making a contribution. You only need to do this one time, and it can be signed by [individual contributors](http://individual-spec-membership.graphql.org/) or diff --git a/packages/vscode-graphql/package.json b/packages/vscode-graphql/package.json index 4af120ef066..4df696837cd 100644 --- a/packages/vscode-graphql/package.json +++ b/packages/vscode-graphql/package.json @@ -85,13 +85,13 @@ "null" ], "default": false, - "description": "Enable debug logs" + "description": "Enable debug logs and node debugger for client" }, "vscode-graphql.cacheSchemaFileForLookup": { "type": [ "boolean" ], - "description": "Use a cached file output of your graphql-config schema result for definition lookups, symbols, outline, etc. Disabled by default." + "description": "Use a cached file output of your graphql-config schema result for definition lookups, symbols, outline, etc. Enabled by default when one or more schema entry is not a local file with SDL in it. Disable if you want to use SDL with a generated schema." }, "vscode-graphql.largeFileThreshold": { "type": [ @@ -111,36 +111,36 @@ "type": [ "string" ], - "description": "Base dir for graphql config loadConfig()" + "description": "Base dir for graphql config loadConfig(), to look for config files or package.json" }, "graphql-config.load.filePath": { "type": [ "string" ], - "description": "filePath for graphql config loadConfig()", - "default": null - }, - "graphql-config.load.legacy": { - "type": [ - "boolean" - ], - "description": "legacy mode for graphql config v2 config", + "description": "exact filePath for a `graphql-config` file `loadConfig()`", "default": null }, "graphql-config.load.configName": { "type": [ "string" ], - "description": "optional .config.js instead of default `graphql`", + "description": "optional .config.{js,ts,toml,yaml,json} & rc* instead of default `graphql`", "default": null }, - "graphql-config.dotEnvPath": { + "graphql-config.load.legacy": { "type": [ - "string" + "boolean" ], - "description": "optional .env load path, if not the default", + "description": "legacy mode for graphql config v2 config", "default": null } + }, + "graphql-config.dotEnvPath": { + "type": [ + "string" + ], + "description": "optional .env load file path, if not the default. specify a relative path to the .env file to be loaded by dotenv module. you can also import dotenv in the config file.", + "default": null } }, "commands": [ From 18ab49621be000af69695f6cd997e7a31f28cbf2 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Tue, 12 Mar 2024 20:16:56 +0100 Subject: [PATCH 46/49] fix: refactor for graphql-tools --- .../src/GraphQLCache.ts | 954 ++++++++++++------ .../src/MessageProcessor.ts | 187 ++-- .../src/__tests__/GraphQLCache-test.ts | 2 +- .../src/__tests__/MessageProcessor.spec.ts | 86 +- .../src/__tests__/MessageProcessor.test.ts | 14 +- .../src/__tests__/parseDocument-test.ts | 20 +- .../src/parseDocument.ts | 8 +- .../graphql-language-service/src/types.ts | 21 +- packages/vscode-graphql/src/extension.ts | 1 + 9 files changed, 892 insertions(+), 401 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 0f181ed0071..078e9b73acc 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -17,6 +17,12 @@ import { extendSchema, parse, visit, + Location, + SourceLocation, + Token, + // Source, + Source as GraphQLSource, + printSchema, } from 'graphql'; import type { CachedContent, @@ -26,9 +32,9 @@ import type { ObjectTypeInfo, Uri, } from 'graphql-language-service'; - -import * as fs from 'node:fs'; -import { readFile } from 'node:fs/promises'; +import { gqlPluckFromCodeString } from '@graphql-tools/graphql-tag-pluck'; +import { Position, Range } from 'graphql-language-service'; +import { readFile, stat, writeFile } from 'node:fs/promises'; import nullthrows from 'nullthrows'; import { @@ -37,6 +43,7 @@ import { GraphQLProjectConfig, GraphQLExtensionDeclaration, } from 'graphql-config'; +import { Source } from '@graphql-tools/utils'; import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load'; @@ -46,17 +53,33 @@ import glob from 'glob'; import { LoadConfigOptions } from './types'; import { URI } from 'vscode-uri'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import { UrlLoader } from '@graphql-tools/url-loader'; + import { DEFAULT_SUPPORTED_EXTENSIONS, DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, + SupportedExtensionsEnum, } from './constants'; import { NoopLogger, Logger } from './Logger'; +import path, { extname, resolve } from 'node:path'; +import { file } from '@babel/types'; +import { + TextDocumentChangeEvent, + TextDocumentContentChangeEvent, +} from 'vscode-languageserver'; +import { existsSync, mkdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { // For schema api.loaders.schema.register(new CodeFileLoader()); + api.loaders.schema.register(new GraphQLFileLoader()); + api.loaders.schema.register(new UrlLoader()); // For documents api.loaders.documents.register(new CodeFileLoader()); + api.loaders.documents.register(new GraphQLFileLoader()); + api.loaders.documents.register(new UrlLoader()); return { name: 'languageService' }; }; @@ -64,16 +87,34 @@ const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { // Maximum files to read when processing GraphQL files. const MAX_READS = 200; +const graphqlRangeFromLocation = (location: Location): Range => { + const locOffset = location.source.locationOffset; + const start = location.startToken; + const end = location.endToken; + return new Range( + new Position( + start.line + locOffset.line - 1, + start.column + locOffset.column - 1, + ), + new Position( + end.line + locOffset.line - 1, + end.column + locOffset.column - 1, + ), + ); +}; + export async function getGraphQLCache({ parser, logger, loadConfigOptions, config, + settings, }: { parser: typeof parseDocument; logger: Logger | NoopLogger; loadConfigOptions: LoadConfigOptions; config?: GraphQLConfig; + settings?: Record; }): Promise { const graphQLConfig = config || @@ -89,6 +130,7 @@ export async function getGraphQLCache({ config: graphQLConfig!, parser, logger, + settings, }); } @@ -96,23 +138,30 @@ export class GraphQLCache { _configDir: Uri; _graphQLFileListCache: Map>; _graphQLConfig: GraphQLConfig; - _schemaMap: Map; + _schemaMap: Map; _typeExtensionMap: Map; _fragmentDefinitionsCache: Map>; _typeDefinitionsCache: Map>; _parser: typeof parseDocument; _logger: Logger | NoopLogger; + private _tmpDir: any; + private _tmpDirBase: string; + private _settings?: Record; constructor({ configDir, config, parser, logger, + tmpDir, + settings, }: { configDir: Uri; config: GraphQLConfig; parser: typeof parseDocument; logger: Logger | NoopLogger; + tmpDir?: string; + settings?: Record; }) { this._configDir = configDir; this._graphQLConfig = config; @@ -123,10 +172,18 @@ export class GraphQLCache { this._typeExtensionMap = new Map(); this._parser = parser; this._logger = logger; + this._tmpDir = tmpDir || tmpdir(); + this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service'); + this._settings = settings; } getGraphQLConfig = (): GraphQLConfig => this._graphQLConfig; + /** + * + * @param uri system protocol path for the file, e.g. file:///path/to/file + * @returns + */ getProjectForFile = (uri: string): GraphQLProjectConfig | void => { try { const project = this._graphQLConfig.getProjectForFile( @@ -145,6 +202,156 @@ export class GraphQLCache { return; } }; + private _getTmpProjectPath( + project: GraphQLProjectConfig, + prependWithProtocol = true, + appendPath?: string, + ) { + const baseDir = this.getGraphQLConfig().dirpath; + const workspaceName = path.basename(baseDir); + const basePath = path.join(this._tmpDirBase, workspaceName); + let projectTmpPath = path.join(basePath, 'projects', project.name); + if (!existsSync(projectTmpPath)) { + mkdirSync(projectTmpPath, { + recursive: true, + }); + } + if (appendPath) { + projectTmpPath = path.join(projectTmpPath, appendPath); + } + if (prependWithProtocol) { + return URI.file(path.resolve(projectTmpPath)).toString(); + } + return path.resolve(projectTmpPath); + } + private _unwrapProjectSchema(project: GraphQLProjectConfig): string[] { + const projectSchema = project.schema; + + const schemas: string[] = []; + if (typeof projectSchema === 'string') { + schemas.push(projectSchema); + } else if (Array.isArray(projectSchema)) { + for (const schemaEntry of projectSchema) { + if (typeof schemaEntry === 'string') { + schemas.push(schemaEntry); + } else if (schemaEntry) { + schemas.push(...Object.keys(schemaEntry)); + } + } + } else { + schemas.push(...Object.keys(projectSchema)); + } + + return schemas.reduce((agg, schema) => { + const results = this._globIfFilePattern(schema); + return [...agg, ...results]; + }, []); + } + private _globIfFilePattern(pattern: string) { + if (pattern.includes('*')) { + try { + return glob.sync(pattern); + // URLs may contain * characters + } catch {} + } + return [pattern]; + } + + private async _cacheConfigSchema(project: GraphQLProjectConfig) { + try { + const schema = await this.getSchema(project.name); + if (schema) { + let schemaText = printSchema(schema); + // file:// protocol path + const uri = this._getTmpProjectPath( + project, + true, + 'generated-schema.graphql', + ); + + // no file:// protocol for fs.writeFileSync() + const fsPath = this._getTmpProjectPath( + project, + false, + 'generated-schema.graphql', + ); + schemaText = `# This is an automatically generated representation of your schema.\n# Any changes to this file will be overwritten and will not be\n# reflected in the resulting GraphQL schema\n\n${schemaText}`; + + const cachedSchemaDoc = this._getCachedDocument(uri, project); + this._schemaMap.set( + this._getSchemaCacheKeyForProject(project) as string, + { schema, localUri: uri }, + ); + if (!cachedSchemaDoc) { + await writeFile(fsPath, schemaText, 'utf8'); + await this._cacheSchemaText(uri, schemaText, 0, project); + } + // do we have a change in the getSchema result? if so, update schema cache + if (cachedSchemaDoc) { + await writeFile(fsPath, schemaText, 'utf8'); + await this._cacheSchemaText( + uri, + schemaText, + cachedSchemaDoc.version++, + project, + ); + } + return { + uri, + fsPath, + }; + } + } catch (err) { + this._logger.error(String(err)); + } + } + _cacheSchemaText = async ( + uri: string, + text: string, + version: number, + project: GraphQLProjectConfig, + ) => { + const projectCacheKey = this._cacheKeyForProject(project); + const projectCache = this._graphQLFileListCache.get(projectCacheKey); + const ast = parse(text); + console.log({ uri }); + if (projectCache) { + const lines = text.split('\n'); + projectCache.set(uri, { + filePath: uri, + fsPath: URI.parse(uri).fsPath, + source: text, + contents: [ + { + documentString: text, + ast, + range: new Range( + new Position(0, 0), + new Position(lines.length, lines.at(-1)?.length), + ), + }, + ], + mtime: Math.trunc(new Date().getTime() / 1000), + size: text.length, + version, + }); + + projectCache.delete(project.schema.toString()); + + this._setDefinitionCache( + [{ documentString: text, ast, range: null }], + this._typeDefinitionsCache.get(projectCacheKey) || new Map(), + uri, + ); + this._graphQLFileListCache.set(projectCacheKey, projectCache); + } + }; + + _getCachedDocument(uri: string, project: GraphQLProjectConfig) { + const projectCacheKey = this._cacheKeyForProject(project); + const projectCache = this._graphQLFileListCache.get(projectCacheKey); + return projectCache?.get(uri); + } getFragmentDependencies = async ( query: string, @@ -157,17 +364,20 @@ export class GraphQLCache { } // If the query cannot be parsed, validations cannot happen yet. // Return an empty array. - let parsedQuery; + let parsedDocument; try { - parsedQuery = parse(query); + parsedDocument = parse(query); } catch { return []; } - return this.getFragmentDependenciesForAST(parsedQuery, fragmentDefinitions); + return this.getFragmentDependenciesForAST( + parsedDocument, + fragmentDefinitions, + ); }; getFragmentDependenciesForAST = async ( - parsedQuery: ASTNode, + parsedDocument: ASTNode, fragmentDefinitions: Map, ): Promise => { if (!fragmentDefinitions) { @@ -177,7 +387,7 @@ export class GraphQLCache { const existingFrags = new Map(); const referencedFragNames = new Set(); - visit(parsedQuery, { + visit(parsedDocument, { FragmentDefinition(node) { existingFrags.set(node.name.value, true); }, @@ -232,11 +442,8 @@ export class GraphQLCache { return this._fragmentDefinitionsCache.get(cacheKey) || new Map(); } - const list = await this._readFilesFromInputDirs(rootDir, projectConfig); - const { fragmentDefinitions, graphQLFileMap } = - await this.readAllGraphQLFiles(list); - + await this._buildCachesFromInputDirs(rootDir, projectConfig); this._fragmentDefinitionsCache.set(cacheKey, fragmentDefinitions); this._graphQLFileListCache.set(cacheKey, graphQLFileMap); @@ -244,7 +451,7 @@ export class GraphQLCache { }; getObjectTypeDependenciesForAST = async ( - parsedQuery: ASTNode, + parsedDocument: ASTNode, objectTypeDefinitions: Map, ): Promise> => { if (!objectTypeDefinitions) { @@ -254,7 +461,7 @@ export class GraphQLCache { const existingObjectTypes = new Map(); const referencedObjectTypes = new Set(); - visit(parsedQuery, { + visit(parsedDocument, { ObjectTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, @@ -275,6 +482,7 @@ export class GraphQLCache { ScalarTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, + InterfaceTypeDefinition(node) { existingObjectTypes.set(node.name.value, true); }, @@ -319,93 +527,380 @@ export class GraphQLCache { if (this._typeDefinitionsCache.has(cacheKey)) { return this._typeDefinitionsCache.get(cacheKey) || new Map(); } - const list = await this._readFilesFromInputDirs(rootDir, projectConfig); const { objectTypeDefinitions, graphQLFileMap } = - await this.readAllGraphQLFiles(list); + await this._buildCachesFromInputDirs(rootDir, projectConfig); this._typeDefinitionsCache.set(cacheKey, objectTypeDefinitions); this._graphQLFileListCache.set(cacheKey, graphQLFileMap); return objectTypeDefinitions; }; - _readFilesFromInputDirs = ( - rootDir: string, - projectConfig: GraphQLProjectConfig, - ): Promise> => { - let pattern: string; - const patterns = this._getSchemaAndDocumentFilePatterns(projectConfig); - - // See https://github.com/graphql/graphql-language-service/issues/221 - // for details on why special handling is required here for the - // documents.length === 1 case. - if (patterns.length === 1) { - // @ts-ignore - pattern = patterns[0]; - } else { - pattern = `{${patterns.join(',')}}`; - } - - return new Promise((resolve, reject) => { - const globResult = new glob.Glob( - pattern, - { - cwd: rootDir, - stat: true, - absolute: false, - ignore: [ - 'generated/relay', - '**/__flow__/**', - '**/__generated__/**', - '**/__github__/**', - '**/__mocks__/**', - '**/node_modules/**', - '**/__flowtests__/**', - ], - }, - error => { - if (error) { - reject(error); + public async readAndCacheFile( + uri: string, + changes?: TextDocumentContentChangeEvent[], + ): Promise<{ + project?: GraphQLProjectConfig; + projectCacheKey?: string; + contents?: Array; + } | null> { + const project = this.getProjectForFile(uri); + if (!project) { + return null; + } + let fileContents = null; + const projectCacheKey = this._cacheKeyForProject(project); + // on file change, patch the file with the changes + // so we can handle any potential new graphql content (as well as re-compute offsets for code files) + // before the changes have been saved to the file system + if (changes) { + // TODO: move this to a standalone function with unit tests! + + try { + const fileText = await readFile(URI.parse(uri).fsPath, { + encoding: 'utf-8', + }); + let newFileText = fileText; + for (const change of changes) { + if ('range' in change) { + // patch file with change range and text + const { start, end } = change.range; + const lines = newFileText.split('\n'); + const startLine = start.line; + const endLine = end.line; + + const before = lines.slice(0, startLine).join('\n'); + const after = lines.slice(endLine + 1).join('\n'); + + newFileText = `${before}${change.text}${after}`; } - }, - ); - globResult.on('end', () => { - resolve( - Object.keys(globResult.statCache) - .filter( - filePath => typeof globResult.statCache[filePath] === 'object', - ) - .filter(filePath => projectConfig.match(filePath)) - .map(filePath => { - // @TODO - // so we have to force this here - // because glob's DefinitelyTyped doesn't use fs.Stats here though - // the docs indicate that is what's there :shrug: - const cacheEntry = globResult.statCache[filePath] as fs.Stats; - return { - filePath: URI.file(filePath).toString(), - mtime: Math.trunc(cacheEntry.mtime.getTime() / 1000), - size: cacheEntry.size, - }; - }), - ); - }); + } + if ( + DEFAULT_SUPPORTED_EXTENSIONS.includes( + extname(uri) as SupportedExtensionsEnum, + ) + ) { + const result = await gqlPluckFromCodeString(uri, newFileText); + const source = new GraphQLSource(result[0].body, result[0].name); + source.locationOffset = result[0].locationOffset; + + const lines = result[0].body.split('\n'); + let document = null; + try { + document = parse(source); + } catch (err) { + console.error(err); + } + console.log({ offset: result[0].locationOffset }); + fileContents = [ + { + rawSDL: result[0].body, + document, + range: graphqlRangeFromLocation({ + source: { + body: result[0].body, + locationOffset: result[0].locationOffset, + name: result[0].name, + }, + startToken: { + line: 0, + column: 0, + }, + endToken: { + line: lines.length, + column: lines.at(-1)?.length, + }, + }), + }, + ]; + } else if ( + DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS.includes( + extname(uri) as SupportedExtensionsEnum, + ) + ) { + try { + const source = new GraphQLSource(newFileText, uri); + const lines = newFileText.split('\n'); + fileContents = [ + { + rawSDL: newFileText, + document: parse(source), + range: { + start: { line: 0, character: 0 }, + end: { + line: lines.length, + character: lines.at(-1)?.length, + }, + }, + }, + ]; + } catch (err) { + console.error(err); + } + } + } catch (err) { + console.error(err); + } + } else { + try { + fileContents = + await project._extensionsRegistry.loaders.schema.loadTypeDefs( + URI.parse(uri).fsPath, + { + cwd: project.dirpath, + includeSources: true, + assumeValid: false, + noLocation: false, + assumeValidSDL: false, + }, + ); + } catch { + fileContents = this._parser( + await readFile(URI.parse(uri).fsPath, { encoding: 'utf-8' }), + uri, + ).map(c => { + return { + rawSDL: c.documentString, + document: c.ast, + range: c.range, + }; + }); + } + + if (!fileContents?.length) { + try { + fileContents = + await project._extensionsRegistry.loaders.documents.loadTypeDefs( + URI.parse(uri).fsPath, + { + cwd: project.dirpath, + includeSources: true, + assumeValid: false, + assumeValidSDL: false, + noLocation: false, + }, + ); + } catch { + fileContents = this._parser( + await readFile(URI.parse(uri).fsPath, { encoding: 'utf-8' }), + uri, + ).map(c => { + return { + rawSDL: c.documentString, + document: c.ast, + range: c.range, + }; + }); + } + } + if (!fileContents.length) { + return null; + } + } + + const asts = fileContents.map((doc: Source) => { + return { + ast: doc.document!, + documentString: doc.document?.loc?.source.body ?? doc.rawSDL, + range: doc.document?.loc + ? graphqlRangeFromLocation(doc.document?.loc) + : doc.range ?? null, + }; + }); + + this._setFragmentCache( + asts, + this._fragmentDefinitionsCache.get(projectCacheKey) || new Map(), + uri, + ); + + this._setDefinitionCache( + asts, + this._typeDefinitionsCache.get(projectCacheKey) || new Map(), + uri, + ); + const { fsPath } = URI.parse(uri); + const stats = await stat(fsPath); + const source = await readFile(fsPath, { encoding: 'utf-8' }); + const projectFileCache = + this._graphQLFileListCache.get(projectCacheKey) ?? new Map(); + const cachedDoc = projectFileCache?.get(uri); + + projectFileCache?.set(uri, { + filePath: uri, + fsPath, + source, + contents: asts, + mtime: Math.trunc(stats.mtime.getTime() / 1000), + size: stats.size, + version: cachedDoc?.version ? cachedDoc.version++ : 0, }); - }; - _getSchemaAndDocumentFilePatterns = (projectConfig: GraphQLProjectConfig) => { - const patterns: string[] = []; + return { + project, + projectCacheKey, + contents: asts, + }; + } + + _buildCachesFromInputDirs = async ( + rootDir: string, + projectConfig: GraphQLProjectConfig, + options?: { maxReads?: number; schemaOnly?: boolean }, + ): Promise<{ + objectTypeDefinitions: Map; + fragmentDefinitions: Map; + graphQLFileMap: Map; + }> => { + try { + let documents: Source[] = []; - for (const pointer of [projectConfig.documents, projectConfig.schema]) { - if (pointer) { - if (typeof pointer === 'string') { - patterns.push(pointer); - } else if (Array.isArray(pointer)) { - patterns.push(...pointer); + if (!options?.schemaOnly) { + try { + documents = + await projectConfig._extensionsRegistry.loaders.documents.loadTypeDefs( + projectConfig.documents, + { + noLocation: false, + assumeValid: false, + assumeValidSDL: false, + includeSources: true, + }, + ); + } catch (err) { + this._logger.log(String(err)); } } - } - return patterns; + let schemaDocuments: Source[] = []; + // cache schema files + try { + schemaDocuments = + await projectConfig._extensionsRegistry.loaders.schema.loadTypeDefs( + projectConfig.schema, + { + noLocation: false, + assumeValid: false, + assumeValidSDL: false, + includeSources: true, + }, + ); + } catch (err) { + this._logger.log(String(err)); + } + + // console.log('schemaDocuments', schemaDocuments); + + documents = [...documents, ...schemaDocuments]; + const graphQLFileMap = new Map(); + const fragmentDefinitions = new Map(); + const objectTypeDefinitions = new Map(); + await Promise.all( + documents.map(async doc => { + if (!doc.rawSDL || !doc.document || !doc.location) { + return; + } + let fsPath = doc.location; + let filePath; + const isNetwork = doc.location.startsWith('http'); + if (!isNetwork) { + try { + fsPath = resolve(rootDir, doc.location); + } catch {} + filePath = URI.file(fsPath).toString(); + } else { + filePath = this._getTmpProjectPath( + projectConfig, + true, + 'generated-schema.graphql', + ); + fsPath = this._getTmpProjectPath( + projectConfig, + false, + 'generated-schema.graphql', + ); + } + + const content = doc.document.loc?.source.body ?? ''; + for (const definition of doc.document.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + fragmentDefinitions.set(definition.name.value, { + filePath, + fsPath, + content, + definition, + }); + } else if (isTypeDefinitionNode(definition)) { + objectTypeDefinitions.set(definition.name.value, { + uri: filePath, + filePath, + fsPath, + content, + definition, + }); + } + } + // console.log(graphqlRangeFromLocation(doc.document.loc)); + if (graphQLFileMap.has(filePath)) { + const cachedEntry = graphQLFileMap.get(filePath)!; + graphQLFileMap.set(filePath, { + ...cachedEntry, + source: content, + contents: [ + ...cachedEntry.contents, + { + ast: doc.document, + documentString: doc.document.loc?.source.body ?? doc.rawSDL, + range: doc.document.loc + ? graphqlRangeFromLocation(doc.document.loc) + : null, + }, + ], + }); + } else { + let mtime = new Date(); + let size = 0; + if (!isNetwork) { + try { + const stats = await stat(fsPath); + mtime = stats.mtime; + size = stats.size; + } catch {} + } + + graphQLFileMap.set(filePath, { + filePath, + fsPath, + source: content, + version: 0, + contents: [ + { + ast: doc.document, + documentString: doc.document.loc?.source.body ?? doc.rawSDL, + range: doc.document.loc + ? graphqlRangeFromLocation(doc.document.loc) + : null, + }, + ], + mtime: Math.trunc(mtime.getTime() / 1000), + size, + }); + } + }), + ); + + return { + graphQLFileMap, + fragmentDefinitions, + objectTypeDefinitions, + }; + } catch (err) { + this._logger.error(`Error building caches from input dirs: ${err}`); + return { + graphQLFileMap: new Map(), + fragmentDefinitions: new Map(), + objectTypeDefinitions: new Map(), + }; + } }; async updateFragmentDefinition( @@ -414,14 +909,15 @@ export class GraphQLCache { contents: Array, ): Promise { const cache = this._fragmentDefinitionsCache.get(projectCacheKey); - const asts = contents.map(({ query }) => { + const asts = contents.map(({ documentString, range, ast }) => { try { return { - ast: parse(query), - query, + ast: ast ?? parse(documentString), + documentString, + range, }; } catch { - return { ast: null, query }; + return { ast: null, documentString, range }; } }); if (cache) { @@ -442,11 +938,11 @@ export class GraphQLCache { } } _setFragmentCache( - asts: { ast: DocumentNode | null; query: string }[], + asts: CachedContent[], fragmentCache: Map, filePath: string | undefined, ) { - for (const { ast, query } of asts) { + for (const { ast, documentString } of asts) { if (!ast) { continue; } @@ -454,7 +950,7 @@ export class GraphQLCache { if (definition.kind === Kind.FRAGMENT_DEFINITION) { fragmentCache.set(definition.name.value, { filePath, - content: query, + content: documentString, definition, }); } @@ -469,14 +965,15 @@ export class GraphQLCache { contents: Array, ): Promise { const cache = this._typeDefinitionsCache.get(projectCacheKey); - const asts = contents.map(({ query }) => { + const asts = contents.map(({ documentString, range, ast }) => { try { return { - ast: parse(query), - query, + ast, + documentString, + range: range ?? null, }; } catch { - return { ast: null, query }; + return { ast: null, documentString, range: range ?? null }; } }); if (cache) { @@ -493,11 +990,11 @@ export class GraphQLCache { } } _setDefinitionCache( - asts: { ast: DocumentNode | null; query: string }[], + asts: CachedContent[], typeCache: Map, filePath: string | undefined, ) { - for (const { ast, query } of asts) { + for (const { ast, documentString } of asts) { if (!ast) { continue; } @@ -505,7 +1002,9 @@ export class GraphQLCache { if (isTypeDefinitionNode(definition)) { typeCache.set(definition.name.value, { filePath, - content: query, + uri: filePath, + fsPath: filePath, + content: documentString, definition, }); } @@ -525,8 +1024,11 @@ export class GraphQLCache { if (!graphQLFileMap) { return schema; } - for (const { filePath, asts } of graphQLFileMap.values()) { - for (const ast of asts) { + for (const { filePath, contents } of graphQLFileMap.values()) { + for (const { ast } of contents) { + if (!ast) { + continue; + } if (filePath === schemaPath) { continue; } @@ -590,49 +1092,90 @@ export class GraphQLCache { const schemaKey = this._getSchemaCacheKeyForProject(projectConfig); let schemaCacheKey = null; - let schema = null; + let schema: { schema?: GraphQLSchema; localUri?: string } = {}; if (schemaPath && schemaKey) { schemaCacheKey = schemaKey as string; + if (this._schemaMap.has(schemaCacheKey)) { + schema = this._schemaMap.get(schemaCacheKey) as { + schema: GraphQLSchema; + localUri?: string; + }; + if (schema.schema) { + return queryHasExtensions + ? this._extendSchema(schema.schema, schemaPath, schemaCacheKey) + : schema.schema; + } + } try { // Read from disk - schema = await projectConfig.getSchema(); + schema.schema = await projectConfig.getSchema(); } catch { // // if there is an error reading the schema, just use the last valid schema - // schema = this._schemaMap.get(schemaCacheKey); - } - - if (this._schemaMap.has(schemaCacheKey)) { schema = this._schemaMap.get(schemaCacheKey); - if (schema) { - return queryHasExtensions - ? this._extendSchema(schema, schemaPath, schemaCacheKey) - : schema; - } } } - const customDirectives = projectConfig?.extensions?.customDirectives; - if (customDirectives && schema) { - const directivesSDL = customDirectives.join('\n\n'); - schema = extendSchema(schema, parse(directivesSDL)); + if (!schema.schema) { + return null; } - if (!schema) { - return null; + const customDirectives = projectConfig?.extensions?.customDirectives; + if (customDirectives) { + const directivesSDL = customDirectives.join('\n\n'); + schema.schema = extendSchema(schema.schema, parse(directivesSDL)); } if (this._graphQLFileListCache.has(this._configDir)) { - schema = this._extendSchema(schema, schemaPath, schemaCacheKey); + schema.schema = this._extendSchema( + schema.schema, + schemaPath, + schemaCacheKey, + ); } if (schemaCacheKey) { - this._schemaMap.set(schemaCacheKey, schema); + this._schemaMap.set( + schemaCacheKey, + schema as { + schema: GraphQLSchema; + localUri?: string; + }, + ); + await this.maybeCacheSchemaFile(projectConfig); } - return schema; + return schema.schema; }; - + private async maybeCacheSchemaFile(projectConfig: GraphQLProjectConfig) { + const cacheSchemaFileForLookup = + projectConfig.extensions.languageService?.cacheSchemaFileForLookup ?? + this?._settings?.cacheSchemaFileForLookup ?? + true; + const unwrappedSchema = this._unwrapProjectSchema(projectConfig); + const allExtensions = [ + ...DEFAULT_SUPPORTED_EXTENSIONS, + ...DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, + ]; + + // // only local schema lookups if all of the schema entries are local files + const sdlOnly = unwrappedSchema.every(schemaEntry => + allExtensions.some( + // local schema file URIs for lookup don't start with http, and end with an extension. + // though it isn't often used, technically schema config could include a remote .graphql file + ext => !schemaEntry.startsWith('http') && schemaEntry.endsWith(ext), + ), + ); + console.log({ unwrappedSchema, sdlOnly, cacheSchemaFileForLookup }); + if (!sdlOnly && cacheSchemaFileForLookup) { + const result = await this._cacheConfigSchema(projectConfig); + if (result) { + const { uri, fsPath } = result; + return { uri, fsPath, sdlOnly }; + } + } + return { sdlOnly }; + } invalidateSchemaCacheForProject(projectConfig: GraphQLProjectConfig) { const schemaKey = this._getSchemaCacheKeyForProject( projectConfig, @@ -651,165 +1194,4 @@ export class GraphQLCache { _getProjectName(projectConfig: GraphQLProjectConfig) { return projectConfig?.name || 'default'; } - - /** - * Given a list of GraphQL file metadata, read all files collected from watchman - * and create fragmentDefinitions and GraphQL files cache. - */ - readAllGraphQLFiles = async ( - list: Array, - ): Promise<{ - objectTypeDefinitions: Map; - fragmentDefinitions: Map; - graphQLFileMap: Map; - }> => { - const queue = list.slice(); // copy - const responses: GraphQLFileInfo[] = []; - while (queue.length) { - const chunk = queue.splice(0, MAX_READS); - const promises = chunk.map(async fileInfo => { - try { - const response = await this.promiseToReadGraphQLFile( - fileInfo.filePath, - ); - responses.push({ - ...response, - mtime: fileInfo.mtime, - size: fileInfo.size, - }); - } catch (error: any) { - // eslint-disable-next-line no-console - console.log('pro', error); - /** - * fs emits `EMFILE | ENFILE` error when there are too many - * open files - this can cause some fragment files not to be - * processed. Solve this case by implementing a queue to save - * files failed to be processed because of `EMFILE` error, - * and await on Promises created with the next batch from the - * queue. - */ - if (error.code === 'EMFILE' || error.code === 'ENFILE') { - queue.push(fileInfo); - } - } - }); - await Promise.all(promises); // eslint-disable-line no-await-in-loop - } - - return this.processGraphQLFiles(responses); - }; - - /** - * Takes an array of GraphQL File information and batch-processes into a - * map of fragmentDefinitions and GraphQL file cache. - */ - processGraphQLFiles = ( - responses: Array, - ): { - objectTypeDefinitions: Map; - fragmentDefinitions: Map; - graphQLFileMap: Map; - } => { - const objectTypeDefinitions = new Map(); - const fragmentDefinitions = new Map(); - const graphQLFileMap = new Map(); - - for (const response of responses) { - const { filePath, content, asts, mtime, size } = response; - - if (asts) { - for (const ast of asts) { - for (const definition of ast.definitions) { - if (definition.kind === Kind.FRAGMENT_DEFINITION) { - fragmentDefinitions.set(definition.name.value, { - filePath, - content, - definition, - }); - } else if (isTypeDefinitionNode(definition)) { - objectTypeDefinitions.set(definition.name.value, { - filePath, - content, - definition, - }); - } - } - } - } - - // Relay the previous object whether or not ast exists. - graphQLFileMap.set(filePath, { - filePath, - content, - asts, - mtime, - size, - }); - } - - return { - objectTypeDefinitions, - fragmentDefinitions, - graphQLFileMap, - }; - }; - - /** - * Returns a Promise to read a GraphQL file and return a GraphQL metadata - * including a parsed AST. - */ - promiseToReadGraphQLFile = async ( - filePath: Uri, - ): Promise => { - const content = await readFile(URI.parse(filePath).fsPath, 'utf8'); - - const asts: DocumentNode[] = []; - let queries: CachedContent[] = []; - if (content.trim().length !== 0) { - try { - queries = this._parser( - content, - filePath, - DEFAULT_SUPPORTED_EXTENSIONS, - DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, - this._logger, - ); - if (queries.length === 0) { - // still resolve with an empty ast - return { - filePath, - content, - asts: [], - queries: [], - mtime: 0, - size: 0, - }; - } - - for (const { query } of queries) { - asts.push(parse(query)); - } - return { - 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. - return { - filePath, - content, - asts: [], - queries: [], - mtime: 0, - size: 0, - }; - } - } - return { filePath, content, asts, queries, mtime: 0, size: 0 }; - }; } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index e97b5a6f2ee..828b1445d46 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -20,6 +20,7 @@ import { Range, Position, IPosition, + GraphQLFileInfo, } from 'graphql-language-service'; import { GraphQLLanguageService } from './GraphQLLanguageService'; @@ -50,6 +51,7 @@ import type { WorkspaceSymbolParams, Connection, DidChangeConfigurationRegistrationOptions, + TextDocumentContentChangeEvent, } from 'vscode-languageserver/node'; import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load'; @@ -248,7 +250,7 @@ export class MessageProcessor { this._graphQLCache = await getGraphQLCache({ parser: this._parser, loadConfigOptions: this._loadConfigOptions, - + settings: this._settings, logger: this._logger, }); this._languageService = new GraphQLLanguageService( @@ -390,8 +392,9 @@ export class MessageProcessor { return { uri, diagnostics }; } try { + console.log('and here'); const project = this._graphQLCache.getProjectForFile(uri); - + console.log('and here 1'); if (project) { const text = 'text' in textDocument && textDocument.text; // for some reason if i try to tell to not parse empty files, it breaks :shrug: @@ -402,19 +405,20 @@ export class MessageProcessor { const { contents } = await this._parseAndCacheFile( uri, project, - text as string, + // text as string, ); + console.log('and here 2'); if (project?.extensions?.languageService?.enableValidation !== false) { await Promise.all( - contents.map(async ({ query, range }) => { + contents.map(async ({ documentString, range }) => { const results = await this._languageService.getDiagnostics( - query, + documentString, uri, - this._isRelayCompatMode(query), + this._isRelayCompatMode(documentString), ); if (results && results.length > 0) { diagnostics.push( - ...processDiagnosticsMessage(results, query, range), + ...processDiagnosticsMessage(results, documentString, range), ); } }), @@ -473,7 +477,7 @@ export class MessageProcessor { const { contents } = await this._parseAndCacheFile( uri, project, - contentChanges.at(-1)!.text, + contentChanges, ); // // If it's a .graphql file, proceed normally and invalidate the cache. // await this._invalidateCache(textDocument, uri, contents); @@ -483,16 +487,17 @@ export class MessageProcessor { if (project?.extensions?.languageService?.enableValidation !== false) { // Send the diagnostics onChange as well try { + console.log({ contents }); await Promise.all( - contents.map(async ({ query, range }) => { + contents.map(async ({ documentString, range }) => { const results = await this._languageService.getDiagnostics( - query, + documentString, uri, - this._isRelayCompatMode(query), + this._isRelayCompatMode(documentString), ); if (results && results.length > 0) { diagnostics.push( - ...processDiagnosticsMessage(results, query, range), + ...processDiagnosticsMessage(results, documentString, range), ); } // skip diagnostic errors, usually related to parsing incomplete fragments @@ -590,30 +595,34 @@ export class MessageProcessor { // Treat the computed list always complete. const cachedDocument = this._getCachedDocument(textDocument.uri); + console.log({ cachedDocument, uri: textDocument.uri }); if (!cachedDocument) { return { items: [], isIncomplete: false }; } const found = cachedDocument.contents.find(content => { const currentRange = content.range; + console.log({ currentRange, position: toPosition(position) }); if (currentRange?.containsPosition(toPosition(position))) { return true; } }); + console.log({ found }); + // If there is no GraphQL query in this file, return an empty result. if (!found) { return { items: [], isIncomplete: false }; } - const { query, range } = found; + const { documentString, range } = found; if (range) { position.line -= range.start.line; } const result = await this._languageService.getAutocompleteSuggestions( - query, + documentString, toPosition(position), textDocument.uri, ); @@ -660,13 +669,13 @@ export class MessageProcessor { return { contents: [] }; } - const { query, range } = found; + const { documentString, range } = found; if (range) { position.line -= range.start.line; } const result = await this._languageService.getHoverInformation( - query, + documentString, toPosition(position), textDocument.uri, { useMarkdown: true }, @@ -680,18 +689,24 @@ export class MessageProcessor { private async _parseAndCacheFile( uri: string, project: GraphQLProjectConfig, - text?: string, + changes?: TextDocumentContentChangeEvent[], ) { try { - const fileText = text || (await readFile(URI.parse(uri).fsPath, 'utf-8')); - const contents = this._parser(fileText, uri); - const cachedDocument = this._textDocumentCache.get(uri); - const version = cachedDocument ? cachedDocument.version++ : 0; - await this._invalidateCache({ uri, version }, uri, contents); - await this._updateFragmentDefinition(uri, contents); - await this._updateObjectTypeDefinition(uri, contents, project); + // const fileText = text || (await readFile(URI.parse(uri).fsPath, 'utf-8')); + // const contents = this._parser(fileText, uri); + // const cachedDocument = this._textDocumentCache.get(uri); + // const version = cachedDocument ? cachedDocument.version++ : 0; + // await this._invalidateCache({ uri, version }, uri, contents); + // await this._updateFragmentDefinition(uri, contents); + // await this._updateObjectTypeDefinition(uri, contents, project); + + const result = await this._graphQLCache.readAndCacheFile(uri, changes); await this._updateSchemaIfChanged(project, uri); - return { contents, version }; + + if (result) { + return { contents: result.contents ?? [], version: 0 }; + } + return { contents: [], version: 0 }; } catch { return { contents: [], version: 0 }; } @@ -718,20 +733,25 @@ export class MessageProcessor { if (project) { // Important! Use system file uri not file path here!!!! const { contents } = await this._parseAndCacheFile(uri, project); + console.log({ contents, uri }, 'watched'); if ( project?.extensions?.languageService?.enableValidation !== false ) { diagnostics = ( await Promise.all( - contents.map(async ({ query, range }) => { + contents.map(async ({ documentString, range }) => { const results = await this._languageService.getDiagnostics( - query, + documentString, uri, - this._isRelayCompatMode(query), + this._isRelayCompatMode(documentString), ); if (results && results.length > 0) { - return processDiagnosticsMessage(results, query, range); + return processDiagnosticsMessage( + results, + documentString, + range, + ); } return []; }), @@ -746,6 +766,8 @@ export class MessageProcessor { return { uri, diagnostics: [] }; } if (change.type === FileChangeTypeKind.Deleted) { + const cache = await this._getDocumentCacheForFile(change.uri); + cache?.delete(change.uri); await this._updateFragmentDefinition(change.uri, []); await this._updateObjectTypeDefinition(change.uri, []); } @@ -775,42 +797,51 @@ export class MessageProcessor { const { textDocument, position } = params; const project = this._graphQLCache.getProjectForFile(textDocument.uri); const cachedDocument = this._getCachedDocument(textDocument.uri); + console.log({ cachedDocument }); if (!cachedDocument) { return []; } const found = cachedDocument.contents.find(content => { - const currentRange = content.range; - if (currentRange?.containsPosition(toPosition(position))) { + console.log(content.range, toPosition(position)); + const currentRange = content?.range; + if ( + currentRange && + currentRange?.containsPosition(toPosition(position)) + ) { return true; } }); + console.log({ found }, 'definition'); + // If there is no GraphQL query in this file, return an empty result. if (!found) { return []; } - const { query, range: parentRange } = found; - if (parentRange) { - position.line -= parentRange.start.line; - } + const { documentString, range: parentRange } = found; + // if (parentRange) { + // position.line -= parentRange.start.line; + // } let result = null; try { result = await this._languageService.getDefinition( - query, + documentString, toPosition(position), textDocument.uri, ); - } catch { + console.log({ result }); + } catch (err) { + console.error(err); // these thrown errors end up getting fired before the service is initialized, so lets cool down on that } const inlineFragments: string[] = []; try { - visit(parse(query), { + visit(parse(documentString), { FragmentDefinition(node: FragmentDefinitionNode) { inlineFragments.push(node.name.value); }, @@ -822,7 +853,7 @@ export class MessageProcessor { const defRange = res.range as Range; if (parentRange && res.name) { - const isInline = inlineFragments.includes(res.name); + const isInline = inlineFragments?.includes(res.name); const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes( path.extname(res.path) as SupportedExtensionsEnum, ); @@ -834,6 +865,7 @@ export class MessageProcessor { const vOffset = isEmbedded ? cachedDoc?.contents[0].range?.start.line ?? 0 : parentRange.start.line; + console.log({ defRange }); defRange.setStart( (defRange.start.line += vOffset), @@ -883,7 +915,7 @@ export class MessageProcessor { if ( this._settings.largeFileThreshold !== undefined && this._settings.largeFileThreshold < - cachedDocument.contents[0].query.length + cachedDocument.contents[0].documentString.length ) { return []; } @@ -897,7 +929,7 @@ export class MessageProcessor { ); return this._languageService.getDocumentSymbols( - cachedDocument.contents[0].query, + cachedDocument.contents[0].documentString, textDocument.uri, ); } @@ -941,7 +973,7 @@ export class MessageProcessor { return []; } const docSymbols = await this._languageService.getDocumentSymbols( - cachedDocument.contents[0].query, + cachedDocument.contents[0].documentString, uri, ); symbols.push(...docSymbols); @@ -1095,7 +1127,7 @@ export class MessageProcessor { schemaText = `# This is an automatically generated representation of your schema.\n# Any changes to this file will be overwritten and will not be\n# reflected in the resulting GraphQL schema\n\n${schemaText}`; const cachedSchemaDoc = this._getCachedDocument(uri); - this._graphQLCache._schemaMap.set(project.name, schema); + this._graphQLCache._schemaMap.set(project.name, { schema }); if (!cachedSchemaDoc) { await writeFile(fsPath, schemaText, 'utf8'); await this._cacheSchemaText(uri, schemaText, 0, project); @@ -1141,7 +1173,7 @@ export class MessageProcessor { // I would use the already existing graphql-config AST, but there are a few reasons we can't yet const contents = this._parser(document.rawSDL, uri); - if (!contents[0]?.query) { + if (!contents[0]?.documentString) { return; } await this._updateObjectTypeDefinition(uri, contents); @@ -1166,11 +1198,26 @@ export class MessageProcessor { return Promise.all( Object.keys(config.projects).map(async projectName => { const project = config.getProject(projectName); + const cacheKey = this._graphQLCache._cacheKeyForProject(project); + const { objectTypeDefinitions, graphQLFileMap, fragmentDefinitions } = + await this._graphQLCache._buildCachesFromInputDirs( + project.dirpath, + project, + ); - await this._cacheSchemaFilesForProject(project); - if (project.documents?.length) { - await this._cacheDocumentFilesforProject(project); - } else { + this._graphQLCache._typeDefinitionsCache.set( + cacheKey, + objectTypeDefinitions, + ); + this._graphQLCache._graphQLFileListCache.set( + cacheKey, + graphQLFileMap, + ); + this._graphQLCache._fragmentDefinitionsCache.set( + cacheKey, + fragmentDefinitions, + ); + if (!project.documents) { this._logger.warn( [ `No 'documents' config found for project: ${projectName}.`, @@ -1185,7 +1232,7 @@ export class MessageProcessor { } _isRelayCompatMode(query: string): boolean { return ( - query.includes('RelayCompat') || query.includes('react-relay/compat') + query?.includes('RelayCompat') || query?.includes('react-relay/compat') ); } @@ -1274,12 +1321,23 @@ export class MessageProcessor { } } + private _getDocumentCacheForFile( + uri: string, + ): Map | undefined { + const project = this._graphQLCache.getProjectForFile(uri); + if (project) { + return this._graphQLCache._graphQLFileListCache.get( + this._graphQLCache._cacheKeyForProject(project), + ); + } + } + private _getCachedDocument(uri: string): CachedDocumentType | null { - if (this._textDocumentCache.has(uri)) { - const cachedDocument = this._textDocumentCache.get(uri); - if (cachedDocument) { - return cachedDocument; - } + const fileCache = this._getDocumentCacheForFile(uri); + console.log(fileCache); + const cachedDocument = fileCache?.get(uri); + if (cachedDocument) { + return cachedDocument; } return null; @@ -1289,8 +1347,21 @@ export class MessageProcessor { uri: Uri, contents: CachedContent[], ): Promise | null> { - if (this._textDocumentCache.has(uri)) { - const cachedDocument = this._textDocumentCache.get(uri); + console.log('invalidate'); + let documentCache = this._getDocumentCacheForFile(uri); + if (!documentCache) { + const project = await this._graphQLCache.getProjectForFile(uri); + if (!project) { + return null; + } + documentCache = new Map(); + this._graphQLCache._graphQLFileListCache.set( + this._graphQLCache._cacheKeyForProject(project), + documentCache, + ); + } + if (documentCache?.has(uri)) { + const cachedDocument = documentCache.get(uri); if ( cachedDocument && textDocument && @@ -1299,13 +1370,13 @@ export class MessageProcessor { ) { // Current server capabilities specify the full sync of the contents. // Therefore always overwrite the entire content. - return this._textDocumentCache.set(uri, { + return documentCache.set(uri, { version: textDocument.version, contents, }); } } - return this._textDocumentCache.set(uri, { + return documentCache.set(uri, { version: textDocument.version ?? 0, contents, }); diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts index 54082249e3f..146d9c3cc37 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts @@ -179,7 +179,7 @@ describe('GraphQLCache', () => { '});'; const contents = parseDocument(text, 'test.js'); const result = await cache.getFragmentDependenciesForAST( - parse(contents[0].query), + parse(contents[0].documentString), fragmentDefinitions, ); expect(result.length).toEqual(2); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index ffa97eb78e2..9da9bee3700 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -93,7 +93,7 @@ describe('MessageProcessor with no config', () => { }); }); -describe('project with simple config and graphql files', () => { +describe('the lsp', () => { let app; afterEach(() => { mockfs.restore(); @@ -123,10 +123,16 @@ describe('project with simple config and graphql files', () => { expect(results.diagnostics[1].message).toEqual( 'Fragment "B" cannot be spread here as objects of type "Query" can never be of type "Foo".', ); + console.log( + 'schema', + project.lsp._getCachedDocument(project.uri('schema.graphql')), + ); const initSchemaDefRequest = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('schema.graphql') }, position: { character: 19, line: 0 }, }); + const typeCache1 = project.lsp._graphQLCache._typeDefinitionsCache; + console.log('schema1', typeCache1); expect(initSchemaDefRequest.length).toEqual(1); expect(initSchemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); expect(serializeRange(initSchemaDefRequest[0].range)).toEqual( @@ -135,10 +141,10 @@ describe('project with simple config and graphql files', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); // TODO: for some reason the cache result formats the graphql query?? - const docCache = project.lsp._textDocumentCache; - expect( - docCache.get(project.uri('query.graphql'))!.contents[0].query, - ).toContain('...B'); + // const docCache = project.lsp._graphQLCache._getDocumentCache('default'); + // expect( + // docCache.get(project.uri('query.graphql'))!.contents[0].documentString, + // ).toContain('...B'); const schemaDefinitions = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('fragments.graphql') }, position: { character: 16, line: 0 }, @@ -161,8 +167,8 @@ describe('project with simple config and graphql files', () => { character: 0, }, end: { - line: 2, - character: 1, + line: 0, + character: 25, }, }); // change the file to make the fragment invalid @@ -197,11 +203,11 @@ describe('project with simple config and graphql files', () => { ); // change the file to make the fragment invalid - project.changeFile( - 'schema.graphql', - // now Foo has a bad field, the fragment should be invalid - 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', - ); + // project.changeFile( + // 'schema.graphql', + // // now Foo has a bad field, the fragment should be invalid + // 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', + // ); // await project.lsp.handleWatchedFilesChangedNotification({ // changes: [ // { @@ -210,11 +216,18 @@ describe('project with simple config and graphql files', () => { // }, // ], // }); + + const newSchema = + 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }'; await project.lsp.handleDidChangeNotification({ contentChanges: [ { type: FileChangeType.Changed, - text: 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', + text: newSchema, + range: { + start: { line: 0, character: 0 }, + end: { line: newSchema.split('\n').length, character: 21 }, + }, }, ], textDocument: { uri: project.uri('schema.graphql'), version: 1 }, @@ -297,7 +310,7 @@ describe('project with simple config and graphql files', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); - it('caches files and schema with a URL config', async () => { + it.only('caches files and schema with a URL config', async () => { const project = new MockProject({ files: [ ['query.graphql', 'query { test { isTest, ...T } }'], @@ -314,18 +327,26 @@ describe('project with simple config and graphql files', () => { expect(initParams.diagnostics).toEqual([]); + // schema file is present and contains schema + const file = await readFile(join(genSchemaPath), { encoding: 'utf-8' }); + expect(file.split('\n').length).toBeGreaterThan(10); + expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); + const changeParams = await project.lsp.handleDidChangeNotification({ textDocument: { uri: project.uri('query.graphql'), version: 1 }, - contentChanges: [{ text: 'query { test { isTest, ...T or } }' }], + contentChanges: [ + { + text: 'query { test { isTest, ...T or } }', + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 35 }, + }, + }, + ], }); expect(changeParams?.diagnostics[0].message).toEqual( 'Cannot query field "or" on type "Test".', ); - expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); - - // schema file is present and contains schema - const file = await readFile(join(genSchemaPath), { encoding: 'utf-8' }); - expect(file.split('\n').length).toBeGreaterThan(10); // hover works const hover = await project.lsp.handleHoverRequest({ @@ -373,7 +394,7 @@ describe('project with simple config and graphql files', () => { }); const schemaDefs = await project.lsp.handleDefinitionRequest({ - textDocument: { uri: URI.parse(genSchemaPath).toString() }, + textDocument: { uri: URI.file(genSchemaPath).toString() }, position: { character: 20, line: 17 }, }); expect(schemaDefs[0].uri).toEqual(URI.parse(genSchemaPath).toString()); @@ -398,11 +419,11 @@ describe('project with simple config and graphql files', () => { true, ); - await project.lsp.handleWatchedFilesChangedNotification({ - changes: [ - { uri: project.uri('fragments.ts'), type: FileChangeType.Created }, - ], - }); + // await project.lsp.handleWatchedFilesChangedNotification({ + // changes: [ + // { uri: project.uri('fragments.ts'), type: FileChangeType.Created }, + // ], + // }); const defsForTs = await project.lsp.handleDefinitionRequest({ textDocument: { uri: project.uri('query.graphql') }, position: { character: 26, line: 0 }, @@ -460,10 +481,10 @@ describe('project with simple config and graphql files', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); expect(await project.lsp._graphQLCache.getSchema('a')).toBeDefined(); - const file = await readFile(join(genSchemaPath.replace('default', 'a')), { - encoding: 'utf-8', - }); - expect(file.split('\n').length).toBeGreaterThan(10); + // const file = await readFile(join(genSchemaPath.replace('default', 'a')), { + // encoding: 'utf-8', + // }); + // expect(file.split('\n').length).toBeGreaterThan(10); // add a new typescript file with empty query to the b project // and expect autocomplete to only show options for project b await project.addFile( @@ -515,10 +536,11 @@ describe('project with simple config and graphql files', () => { // { text: schemaFile[1] + '\ntype Example1 { field: }' }, // ], // }); - // console.log(project.fileCache.get('b/schema.graphql')); + console.log(project.fileCache.get('b/schema.graphql')); + console.log(project.lsp._graphQLCache.getSchema('b')); const schemaCompletion = await project.lsp.handleCompletionRequest({ textDocument: { uri: project.uri('b/schema.graphql') }, - position: { character: 25, line: 5 }, + position: { character: 24, line: 5 }, }); // TODO: SDL completion still feels incomplete here... where is Int? // where is self-referential Example1? 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 b22af3ff772..8276a163bb5 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -196,12 +196,12 @@ describe('MessageProcessor', () => { }); it('runs completion requests properly', async () => { const uri = `${queryPathUri}/test2.graphql`; - const query = 'test'; + const documentString = 'test'; messageProcessor._textDocumentCache.set(uri, { version: 0, contents: [ { - query, + documentString, range: new Range(new Position(0, 0), new Position(0, 0)), }, ], @@ -213,7 +213,7 @@ describe('MessageProcessor', () => { }; const result = await messageProcessor.handleCompletionRequest(test); expect(result).toEqual({ - items: [{ label: `${query} at ${uri}` }], + items: [{ label: `${documentString} at ${uri}` }], isIncomplete: false, }); }); @@ -264,7 +264,7 @@ describe('MessageProcessor', () => { version: 0, contents: [ { - query: validQuery, + documentString: validQuery, range: new Range(new Position(0, 0), new Position(0, 0)), }, ], @@ -319,7 +319,7 @@ describe('MessageProcessor', () => { version: 1, contents: [ { - query: '', + documentString: '', range: new Range(new Position(0, 0), new Position(0, 0)), }, ], @@ -394,7 +394,7 @@ describe('MessageProcessor', () => { version: 1, contents: [ { - query: validQuery, + documentString: validQuery, range: new Range(new Position(0, 0), new Position(20, 4)), }, ], @@ -430,7 +430,7 @@ describe('MessageProcessor', () => { version: 1, contents: [ { - query: validQuery, + documentString: validQuery, range: new Range(new Position(0, 0), new Position(20, 4)), }, ], diff --git a/packages/graphql-language-service-server/src/__tests__/parseDocument-test.ts b/packages/graphql-language-service-server/src/__tests__/parseDocument-test.ts index 0020f76d47e..07c3e426b84 100644 --- a/packages/graphql-language-service-server/src/__tests__/parseDocument-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/parseDocument-test.ts @@ -21,7 +21,7 @@ describe('parseDocument', () => { export function Example(arg: string) {}`; const contents = parseDocument(text, 'test.js'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -49,7 +49,7 @@ describe('parseDocument', () => { export function Example(arg: string) {}`; const contents = parseDocument(text, 'test.js'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { @@ -77,7 +77,7 @@ describe('parseDocument', () => { export function Example(arg: string) {}`; const contents = parseDocument(text, 'test.ts'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -109,7 +109,7 @@ describe('parseDocument', () => { }`; const contents = parseDocument(text, 'test.tsx'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -142,7 +142,7 @@ describe('parseDocument', () => { const contents = parseDocument(text, 'test.tsx'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -175,7 +175,7 @@ describe('parseDocument', () => { }`; const contents = parseDocument(text, 'test.tsx'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -210,7 +210,7 @@ describe('parseDocument', () => { }`; const contents = parseDocument(text, 'test.tsx'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -241,7 +241,7 @@ describe('parseDocument', () => { export function Example(arg: string) {}`; const contents = parseDocument(text, 'test.js'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value @@ -270,7 +270,7 @@ describe('parseDocument', () => { export function Example(arg: string) {}`; const contents = parseDocument(text, 'test.ts'); - expect(contents[0].query).toEqual(`#graphql + expect(contents[0].documentString).toEqual(`#graphql query Test { test { value @@ -299,7 +299,7 @@ describe('parseDocument', () => { export function Example(arg: string) {}`; const contents = parseDocument(text, 'test.ts'); - expect(contents[0].query).toEqual(` + expect(contents[0].documentString).toEqual(` query Test { test { value diff --git a/packages/graphql-language-service-server/src/parseDocument.ts b/packages/graphql-language-service-server/src/parseDocument.ts index 6facdebf18e..c18c0228269 100644 --- a/packages/graphql-language-service-server/src/parseDocument.ts +++ b/packages/graphql-language-service-server/src/parseDocument.ts @@ -10,6 +10,7 @@ import { SupportedExtensionsEnum, } from './constants'; import { NoopLogger } from './Logger'; +import { parse } from 'graphql'; /** * Helper functions to perform requested services from client/server. @@ -36,7 +37,10 @@ export function parseDocument( if (fileExtensions.includes(ext)) { const templates = findGraphQLTags(text, ext, uri, logger); - return templates.map(({ template, range }) => ({ query: template, range })); + return templates.map(({ template, range }) => ({ + documentString: template, + range, + })); } if (graphQLFileExtensions.includes(ext)) { const query = text; @@ -45,7 +49,7 @@ export function parseDocument( new Position(0, 0), new Position(lines.length - 1, lines.at(-1)!.length - 1), ); - return [{ query, range }]; + return [{ documentString: query, range }]; } return []; } diff --git a/packages/graphql-language-service/src/types.ts b/packages/graphql-language-service/src/types.ts index f0d4f906763..5c0134d13b7 100644 --- a/packages/graphql-language-service/src/types.ts +++ b/packages/graphql-language-service/src/types.ts @@ -107,8 +107,9 @@ export interface IRange { containsPosition(position: IPosition): boolean; } export type CachedContent = { - query: string; + documentString: string; range: IRange | null; + ast: DocumentNode | null; }; // GraphQL Language Service related types @@ -121,12 +122,16 @@ export type GraphQLFileMetadata = { }; export type GraphQLFileInfo = { - filePath: Uri; - content: string; - asts: DocumentNode[]; - queries: CachedContent[]; + // file:// uri string + filePath?: Uri; + // file system path + fsPath?: string; + source: string; + // asts: DocumentNode[]; + contents: CachedContent[]; size: number; mtime: number; + version: number; }; export type AllTypeInfo = { @@ -144,7 +149,10 @@ export type AllTypeInfo = { }; export type FragmentInfo = { + // file:// uri string filePath?: Uri; + // file system path + fsPath?: string; content: string; definition: FragmentDefinitionNode; }; @@ -156,7 +164,10 @@ export type NamedTypeInfo = { }; export type ObjectTypeInfo = { + // file:// uri string filePath?: Uri; + // file system path + fsPath?: string; content: string; definition: TypeDefinitionNode; }; diff --git a/packages/vscode-graphql/src/extension.ts b/packages/vscode-graphql/src/extension.ts index 56a244b0eb2..70683342c13 100644 --- a/packages/vscode-graphql/src/extension.ts +++ b/packages/vscode-graphql/src/extension.ts @@ -77,6 +77,7 @@ export async function activate(context: ExtensionContext) { ), // TODO: load ignore // These ignore node_modules and .git by default + // json is included so we can detect potential `graphql` config changes to package.json workspace.createFileSystemWatcher( '**/{*.graphql,*.graphqls,*.gql,*.js,*.mjs,*.cjs,*.esm,*.es,*.es6,*.jsx,*.ts,*.tsx,*.vue,*.svelte,*.cts,*.mts,*.json,*.astro}', ), From 4216bd6f55dd65a7172bfb35d7f875979c71365f Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 17 Mar 2024 09:56:09 +0100 Subject: [PATCH 47/49] fix: further ts refinements --- .../package.json | 3 + .../src/GraphQLCache.ts | 208 +++++++++--------- .../graphql-language-service/src/types.ts | 4 +- yarn.lock | 166 +++++++++++++- 4 files changed, 277 insertions(+), 104 deletions(-) diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json index 17f417f2588..9cf6a8aa398 100644 --- a/packages/graphql-language-service-server/package.json +++ b/packages/graphql-language-service-server/package.json @@ -63,6 +63,9 @@ "vscode-uri": "^3.0.2" }, "devDependencies": { + "@graphql-tools/graphql-file-loader": "^8.0.1", + "@graphql-tools/url-loader": "^8.0.2", + "@graphql-tools/utils": "^10.1.2", "@types/glob": "^8.1.0", "@types/mkdirp": "^1.0.1", "@types/mock-fs": "^4.13.4", diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index 078e9b73acc..f63f404ed14 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -18,19 +18,16 @@ import { parse, visit, Location, - SourceLocation, - Token, - // Source, Source as GraphQLSource, printSchema, } from 'graphql'; import type { CachedContent, - GraphQLFileMetadata, GraphQLFileInfo, FragmentInfo, ObjectTypeInfo, Uri, + IRange, } from 'graphql-language-service'; import { gqlPluckFromCodeString } from '@graphql-tools/graphql-tag-pluck'; import { Position, Range } from 'graphql-language-service'; @@ -42,6 +39,8 @@ import { GraphQLConfig, GraphQLProjectConfig, GraphQLExtensionDeclaration, + DocumentPointer, + SchemaPointer, } from 'graphql-config'; import { Source } from '@graphql-tools/utils'; @@ -63,11 +62,7 @@ import { } from './constants'; import { NoopLogger, Logger } from './Logger'; import path, { extname, resolve } from 'node:path'; -import { file } from '@babel/types'; -import { - TextDocumentChangeEvent, - TextDocumentContentChangeEvent, -} from 'vscode-languageserver'; +import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { existsSync, mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; @@ -85,7 +80,6 @@ const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { }; // Maximum files to read when processing GraphQL files. -const MAX_READS = 200; const graphqlRangeFromLocation = (location: Location): Range => { const locOffset = location.source.locationOffset; @@ -314,7 +308,6 @@ export class GraphQLCache { const projectCacheKey = this._cacheKeyForProject(project); const projectCache = this._graphQLFileListCache.get(projectCacheKey); const ast = parse(text); - console.log({ uri }); if (projectCache) { const lines = text.split('\n'); projectCache.set(uri, { @@ -327,7 +320,7 @@ export class GraphQLCache { ast, range: new Range( new Position(0, 0), - new Position(lines.length, lines.at(-1)?.length), + new Position(lines.length, lines.at(-1).length), ), }, ], @@ -535,6 +528,36 @@ export class GraphQLCache { return objectTypeDefinitions; }; + private async loadTypeDefs( + project: GraphQLProjectConfig, + pointer: + | DocumentPointer + | SchemaPointer + | UnnormalizedTypeDefPointer + | UnnormalizedTypeDefPointer[] + | DocumentPointer[] + | SchemaPointer[] + | string, + target: 'documents' | 'schema', + ): Promise { + if (typeof pointer === 'string') { + try { + const { fsPath } = URI.parse(pointer); + return this.loadTypeDefs(project, fsPath, target); + } catch {} + } + // @ts-expect-error these are always here. better typings soon + return project._extensionsRegistry.loaders[target].loadTypeDefs(pointer, { + cwd: project.dirpath, + includes: project.include, + excludes: project.exclude, + includeSources: true, + assumeValid: false, + noLocation: false, + assumeValidSDL: false, + }); + } + public async readAndCacheFile( uri: string, changes?: TextDocumentContentChangeEvent[], @@ -580,26 +603,27 @@ export class GraphQLCache { ) ) { const result = await gqlPluckFromCodeString(uri, newFileText); - const source = new GraphQLSource(result[0].body, result[0].name); - source.locationOffset = result[0].locationOffset; - const lines = result[0].body.split('\n'); - let document = null; - try { - document = parse(source); - } catch (err) { - console.error(err); - } - console.log({ offset: result[0].locationOffset }); - fileContents = [ - { - rawSDL: result[0].body, + fileContents = result.map(plucked => { + const source = new GraphQLSource(plucked.body, plucked.name); + source.locationOffset = plucked.locationOffset; + let document = null; + try { + document = parse(source); + } catch (err) { + console.error(err); + return; + } + + const lines = plucked.body.split('\n'); + return { + rawSDL: plucked.body, document, range: graphqlRangeFromLocation({ source: { - body: result[0].body, - locationOffset: result[0].locationOffset, - name: result[0].name, + body: plucked.body, + locationOffset: plucked.locationOffset, + name: plucked.name, }, startToken: { line: 0, @@ -607,11 +631,11 @@ export class GraphQLCache { }, endToken: { line: lines.length, - column: lines.at(-1)?.length, + column: lines.at(-1)?.length ?? 0, }, }), - }, - ]; + }; + }); } else if ( DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS.includes( extname(uri) as SupportedExtensionsEnum, @@ -642,17 +666,7 @@ export class GraphQLCache { } } else { try { - fileContents = - await project._extensionsRegistry.loaders.schema.loadTypeDefs( - URI.parse(uri).fsPath, - { - cwd: project.dirpath, - includeSources: true, - assumeValid: false, - noLocation: false, - assumeValidSDL: false, - }, - ); + fileContents = await this.loadTypeDefs(project, uri, 'schema'); } catch { fileContents = this._parser( await readFile(URI.parse(uri).fsPath, { encoding: 'utf-8' }), @@ -665,20 +679,9 @@ export class GraphQLCache { }; }); } - if (!fileContents?.length) { try { - fileContents = - await project._extensionsRegistry.loaders.documents.loadTypeDefs( - URI.parse(uri).fsPath, - { - cwd: project.dirpath, - includeSources: true, - assumeValid: false, - assumeValidSDL: false, - noLocation: false, - }, - ); + fileContents = await this.loadTypeDefs(project, uri, 'documents'); } catch { fileContents = this._parser( await readFile(URI.parse(uri).fsPath, { encoding: 'utf-8' }), @@ -692,20 +695,27 @@ export class GraphQLCache { }); } } - if (!fileContents.length) { - return null; - } + } + if (!fileContents?.length) { + return null; } - const asts = fileContents.map((doc: Source) => { - return { - ast: doc.document!, - documentString: doc.document?.loc?.source.body ?? doc.rawSDL, - range: doc.document?.loc - ? graphqlRangeFromLocation(doc.document?.loc) - : doc.range ?? null, - }; - }); + const asts = fileContents + .map(doc => { + return { + ast: doc?.document, + documentString: doc?.document?.loc?.source.body ?? doc?.rawSDL, + range: doc?.document?.loc + ? graphqlRangeFromLocation(doc?.document?.loc) + : // @ts-expect-error + doc?.range ?? null, + }; + }) + .filter(doc => Boolean(doc.documentString)) as { + documentString: string; + ast?: DocumentNode; + range?: IRange; + }[]; this._setFragmentCache( asts, @@ -754,18 +764,13 @@ export class GraphQLCache { try { let documents: Source[] = []; - if (!options?.schemaOnly) { + if (!options?.schemaOnly && projectConfig.documents) { try { - documents = - await projectConfig._extensionsRegistry.loaders.documents.loadTypeDefs( - projectConfig.documents, - { - noLocation: false, - assumeValid: false, - assumeValidSDL: false, - includeSources: true, - }, - ); + documents = await this.loadTypeDefs( + projectConfig, + projectConfig.documents, + 'documents', + ); } catch (err) { this._logger.log(String(err)); } @@ -774,16 +779,11 @@ export class GraphQLCache { let schemaDocuments: Source[] = []; // cache schema files try { - schemaDocuments = - await projectConfig._extensionsRegistry.loaders.schema.loadTypeDefs( - projectConfig.schema, - { - noLocation: false, - assumeValid: false, - assumeValidSDL: false, - includeSources: true, - }, - ); + schemaDocuments = await this.loadTypeDefs( + projectConfig, + projectConfig.schema, + 'schema', + ); } catch (err) { this._logger.log(String(err)); } @@ -831,7 +831,7 @@ export class GraphQLCache { }); } else if (isTypeDefinitionNode(definition)) { objectTypeDefinitions.set(definition.name.value, { - uri: filePath, + // uri: filePath, filePath, fsPath, content, @@ -909,17 +909,24 @@ export class GraphQLCache { contents: Array, ): Promise { const cache = this._fragmentDefinitionsCache.get(projectCacheKey); - const asts = contents.map(({ documentString, range, ast }) => { - try { - return { - ast: ast ?? parse(documentString), - documentString, - range, - }; - } catch { - return { ast: null, documentString, range }; - } - }); + const asts = contents + .map(({ documentString, range, ast }) => { + try { + return { + ast: ast ?? parse(documentString), + documentString, + range, + }; + } catch { + return { ast: null, documentString, range }; + } + }) + .filter(doc => Boolean(doc.documentString)) as { + documentString: string; + ast?: DocumentNode; + range?: IRange; + }[]; + if (cache) { // first go through the fragment list to delete the ones from this file for (const [key, value] of cache.entries()) { @@ -1113,7 +1120,7 @@ export class GraphQLCache { schema.schema = await projectConfig.getSchema(); } catch { // // if there is an error reading the schema, just use the last valid schema - schema = this._schemaMap.get(schemaCacheKey); + schema = this._schemaMap.get(schemaCacheKey)!; } } @@ -1166,7 +1173,6 @@ export class GraphQLCache { ext => !schemaEntry.startsWith('http') && schemaEntry.endsWith(ext), ), ); - console.log({ unwrappedSchema, sdlOnly, cacheSchemaFileForLookup }); if (!sdlOnly && cacheSchemaFileForLookup) { const result = await this._cacheConfigSchema(projectConfig); if (result) { diff --git a/packages/graphql-language-service/src/types.ts b/packages/graphql-language-service/src/types.ts index 5c0134d13b7..57096358613 100644 --- a/packages/graphql-language-service/src/types.ts +++ b/packages/graphql-language-service/src/types.ts @@ -108,8 +108,8 @@ export interface IRange { } export type CachedContent = { documentString: string; - range: IRange | null; - ast: DocumentNode | null; + range?: IRange; + ast?: DocumentNode; }; // GraphQL Language Service related types diff --git a/yarn.lock b/yarn.lock index 0c29f0c3297..d53b5499606 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2392,7 +2392,7 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== @@ -3533,6 +3533,16 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/batch-execute@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-9.0.4.tgz#11601409c0c33491971fc82592de12390ec58be2" + integrity sha512-kkebDLXgDrep5Y0gK1RN3DMUlLqNhg60OAz0lTCqrYeja6DshxLtLkj+zV4mVbBA4mQOEoBmw6g1LZs3dA84/w== + dependencies: + "@graphql-tools/utils" "^10.0.13" + dataloader "^2.2.2" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/code-file-loader@8.0.3": version "8.0.3" resolved "https://registry.yarnpkg.com/@graphql-tools/code-file-loader/-/code-file-loader-8.0.3.tgz#8e1e8c2fc05c94614ce25c3cee36b3b4ec08bb64" @@ -3557,6 +3567,18 @@ tslib "^2.5.0" value-or-promise "^1.0.12" +"@graphql-tools/delegate@^10.0.4": + version "10.0.4" + resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-10.0.4.tgz#7c38240f11e42ec2dd45d0a569ca6433ce4cb8dc" + integrity sha512-WswZRbQZMh/ebhc8zSomK9DIh6Pd5KbuiMsyiKkKz37TWTrlCOe+4C/fyrBFez30ksq6oFyCeSKMwfrCbeGo0Q== + dependencies: + "@graphql-tools/batch-execute" "^9.0.4" + "@graphql-tools/executor" "^1.2.1" + "@graphql-tools/schema" "^10.0.3" + "@graphql-tools/utils" "^10.0.13" + dataloader "^2.2.2" + tslib "^2.5.0" + "@graphql-tools/executor-graphql-ws@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.0.1.tgz#d0ff56518ae2ff2087f35f91b07a03f137905bc9" @@ -3570,6 +3592,18 @@ tslib "^2.4.0" ws "8.13.0" +"@graphql-tools/executor-graphql-ws@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.1.2.tgz#2bf959d2319692460b39400c0fe1515dfbb9f034" + integrity sha512-+9ZK0rychTH1LUv4iZqJ4ESbmULJMTsv3XlFooPUngpxZkk00q6LqHKJRrsLErmQrVaC7cwQCaRBJa0teK17Lg== + dependencies: + "@graphql-tools/utils" "^10.0.13" + "@types/ws" "^8.0.0" + graphql-ws "^5.14.0" + isomorphic-ws "^5.0.0" + tslib "^2.4.0" + ws "^8.13.0" + "@graphql-tools/executor-http@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/executor-http/-/executor-http-1.0.0.tgz#3d7f1ce70dcc40432fb92b970bd1ab4dd1c37b12" @@ -3584,6 +3618,19 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/executor-http@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-http/-/executor-http-1.0.9.tgz#87ca8b99a32241eb0cc30a9c500d2672e92d58b7" + integrity sha512-+NXaZd2MWbbrWHqU4EhXcrDbogeiCDmEbrAN+rMn4Nu2okDjn2MTFDbTIab87oEubQCH4Te1wDkWPKrzXup7+Q== + dependencies: + "@graphql-tools/utils" "^10.0.13" + "@repeaterjs/repeater" "^3.0.4" + "@whatwg-node/fetch" "^0.9.0" + extract-files "^11.0.0" + meros "^1.2.1" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/executor-legacy-ws@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.0.1.tgz#49764812fc93f401cb3f3ef32b2d6db4a9cd8db5" @@ -3595,6 +3642,17 @@ tslib "^2.4.0" ws "8.13.0" +"@graphql-tools/executor-legacy-ws@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.0.6.tgz#4ed311b731db8fd5c99e66a66361afbf9c2109fc" + integrity sha512-lDSxz9VyyquOrvSuCCnld3256Hmd+QI2lkmkEv7d4mdzkxkK4ddAWW1geQiWrQvWmdsmcnGGlZ7gDGbhEExwqg== + dependencies: + "@graphql-tools/utils" "^10.0.13" + "@types/ws" "^8.0.0" + isomorphic-ws "^5.0.0" + tslib "^2.4.0" + ws "^8.15.0" + "@graphql-tools/executor@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-1.1.0.tgz#bafddb7c56d8250c5eda83437c10664e702109a8" @@ -3606,6 +3664,17 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/executor@^1.2.1": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-1.2.2.tgz#08796bb70f3c0a480d446cc9d01e5694899f7450" + integrity sha512-wZkyjndwlzi01HTU3PDveoucKA8qVO0hdKmJhjIGK/vRN/A4w5rDdeqRGcyXVss0clCAy3R6jpixCVu5pWs2Qg== + dependencies: + "@graphql-tools/utils" "^10.1.1" + "@graphql-typed-document-node/core" "3.2.0" + "@repeaterjs/repeater" "^3.0.4" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/graphql-file-loader@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.0.tgz#a2026405bce86d974000455647511bf65df4f211" @@ -3617,6 +3686,17 @@ tslib "^2.4.0" unixify "^1.0.0" +"@graphql-tools/graphql-file-loader@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.1.tgz#03869b14cb91d0ef539df8195101279bb2df9c9e" + integrity sha512-7gswMqWBabTSmqbaNyWSmRRpStWlcCkBc73E6NZNlh4YNuiyKOwbvSkOUYFOqFMfEL+cFsXgAvr87Vz4XrYSbA== + dependencies: + "@graphql-tools/import" "7.0.1" + "@graphql-tools/utils" "^10.0.13" + globby "^11.0.3" + tslib "^2.4.0" + unixify "^1.0.0" + "@graphql-tools/graphql-tag-pluck@8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.1.0.tgz#0745b6f0103eb725f10c5d4c1a9438670bb8e05b" @@ -3639,6 +3719,15 @@ resolve-from "5.0.0" tslib "^2.4.0" +"@graphql-tools/import@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-7.0.1.tgz#4e0d181c63350b1c926ae91b84a4cbaf03713c2c" + integrity sha512-935uAjAS8UAeXThqHfYVr4HEAp6nHJ2sximZKO1RzUTq5WoALMAhhGARl0+ecm6X+cqNUwIChJbjtaa6P/ML0w== + dependencies: + "@graphql-tools/utils" "^10.0.13" + resolve-from "5.0.0" + tslib "^2.4.0" + "@graphql-tools/json-file-loader@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/json-file-loader/-/json-file-loader-8.0.0.tgz#9b1b62902f766ef3f1c9cd1c192813ea4f48109c" @@ -3667,6 +3756,14 @@ "@graphql-tools/utils" "^10.0.0" tslib "^2.4.0" +"@graphql-tools/merge@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.0.3.tgz#4d0b467132e6f788b69fab803d31480b8ce4b61a" + integrity sha512-FeKv9lKLMwqDu0pQjPpF59GY3HReUkWXKsMIuMuJQOKh9BETu7zPEFUELvcw8w+lwZkl4ileJsHXC9+AnsT2Lw== + dependencies: + "@graphql-tools/utils" "^10.0.13" + tslib "^2.4.0" + "@graphql-tools/schema@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.0.tgz#7b5f6b6a59f51c927de8c9069bde4ebbfefc64b3" @@ -3677,6 +3774,16 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/schema@^10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.3.tgz#48c14be84cc617c19c4c929258672b6ab01768de" + integrity sha512-p28Oh9EcOna6i0yLaCFOnkcBDQECVf3SCexT6ktb86QNj9idnkhI+tCxnwZDh58Qvjd2nURdkbevvoZkvxzCog== + dependencies: + "@graphql-tools/merge" "^9.0.3" + "@graphql-tools/utils" "^10.0.13" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/url-loader@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-8.0.0.tgz#8d952d5ebb7325e587cb914aaebded3dbd078cf6" @@ -3696,6 +3803,25 @@ value-or-promise "^1.0.11" ws "^8.12.0" +"@graphql-tools/url-loader@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/url-loader/-/url-loader-8.0.2.tgz#ee8e10a85d82c72662f6bc6bbc7b408510a36ebd" + integrity sha512-1dKp2K8UuFn7DFo1qX5c1cyazQv2h2ICwA9esHblEqCYrgf69Nk8N7SODmsfWg94OEaI74IqMoM12t7eIGwFzQ== + dependencies: + "@ardatan/sync-fetch" "^0.0.1" + "@graphql-tools/delegate" "^10.0.4" + "@graphql-tools/executor-graphql-ws" "^1.1.2" + "@graphql-tools/executor-http" "^1.0.9" + "@graphql-tools/executor-legacy-ws" "^1.0.6" + "@graphql-tools/utils" "^10.0.13" + "@graphql-tools/wrap" "^10.0.2" + "@types/ws" "^8.0.0" + "@whatwg-node/fetch" "^0.9.0" + isomorphic-ws "^5.0.0" + tslib "^2.4.0" + value-or-promise "^1.0.11" + ws "^8.12.0" + "@graphql-tools/utils@^10.0.0": version "10.0.1" resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.0.1.tgz#52e6c0ce920b57473823e487184f5017974fe4c4" @@ -3704,6 +3830,16 @@ "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" +"@graphql-tools/utils@^10.0.13", "@graphql-tools/utils@^10.1.1", "@graphql-tools/utils@^10.1.2": + version "10.1.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.1.2.tgz#192de00e7301c0242e7305ab16bbeef76bbcec74" + integrity sha512-fX13CYsDnX4yifIyNdiN0cVygz/muvkreWWem6BBw130+ODbRRgfiVveL0NizCEnKXkpvdeTy9Bxvo9LIKlhrw== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + cross-inspect "1.0.0" + dset "^3.1.2" + tslib "^2.4.0" + "@graphql-tools/wrap@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-10.0.0.tgz#573ab111482387d4acf4757d5fb7f9553a504bc1" @@ -3715,6 +3851,17 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/wrap@^10.0.2": + version "10.0.5" + resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-10.0.5.tgz#614b964a158887b4a644f5425b2b9a57b5751f72" + integrity sha512-Cbr5aYjr3HkwdPvetZp1cpDWTGdD1Owgsb3z/ClzhmrboiK86EnQDxDvOJiQkDCPWE9lNBwj8Y4HfxroY0D9DQ== + dependencies: + "@graphql-tools/delegate" "^10.0.4" + "@graphql-tools/schema" "^10.0.3" + "@graphql-tools/utils" "^10.1.1" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-typed-document-node/core@3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" @@ -8402,6 +8549,13 @@ cross-env@^7.0.2: dependencies: cross-spawn "^7.0.1" +cross-inspect@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cross-inspect/-/cross-inspect-1.0.0.tgz#5fda1af759a148594d2d58394a9e21364f6849af" + integrity sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ== + dependencies: + tslib "^2.4.0" + cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -11436,6 +11590,11 @@ graphql-ws@5.14.0: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.14.0.tgz#766f249f3974fc2c48fae0d1fb20c2c4c79cd591" integrity sha512-itrUTQZP/TgswR4GSSYuwWUzrE/w5GhbwM2GX3ic2U7aw33jgEsayfIlvaj7/GcIvZgNMzsPTrE5hqPuFUiE5g== +graphql-ws@^5.14.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.15.0.tgz#2db79e1b42468a8363bf5ca6168d076e2f8fdebc" + integrity sha512-xWGAtm3fig9TIhSaNsg0FaDZ8Pyn/3re3RFlP4rhQcmjRDIPpk1EhRuNB+YSJtLzttyuToaDiNhwT1OMoGnJnw== + graphql-ws@^5.5.5: version "5.5.5" resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.5.5.tgz#f375486d3f196e2a2527b503644693ae3a8670a9" @@ -20483,6 +20642,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.13.0, ws@^8.15.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + ws@^8.4.2: version "8.12.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" From 310048b4e4a776bf959ee0b2f3aaf508e253a5b1 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 17 Mar 2024 10:10:11 +0100 Subject: [PATCH 48/49] fix: more cleanup --- .../package.json | 7 +-- .../src/GraphQLCache.ts | 12 ++--- .../src/MessageProcessor.ts | 48 ++++++------------- .../graphql-language-service/src/types.ts | 1 + yarn.lock | 41 ++++++++++------ 5 files changed, 53 insertions(+), 56 deletions(-) diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json index 9cf6a8aa398..704fc323df5 100644 --- a/packages/graphql-language-service-server/package.json +++ b/packages/graphql-language-service-server/package.json @@ -42,6 +42,10 @@ "@babel/parser": "^7.23.6", "@babel/types": "^7.23.5", "@graphql-tools/code-file-loader": "8.0.3", + "@graphql-tools/graphql-tag-pluck": "^8.3.0", + "@graphql-tools/graphql-file-loader": "^8.0.1", + "@graphql-tools/url-loader": "^8.0.2", + "@graphql-tools/utils": "^10.1.2", "@vue/compiler-sfc": "^3.4.5", "astrojs-compiler-sync": "^0.3.5", "cosmiconfig-toml-loader": "^1.0.0", @@ -63,9 +67,6 @@ "vscode-uri": "^3.0.2" }, "devDependencies": { - "@graphql-tools/graphql-file-loader": "^8.0.1", - "@graphql-tools/url-loader": "^8.0.2", - "@graphql-tools/utils": "^10.1.2", "@types/glob": "^8.1.0", "@types/mkdirp": "^1.0.1", "@types/mock-fs": "^4.13.4", diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index f63f404ed14..d02da820712 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -802,12 +802,7 @@ export class GraphQLCache { let fsPath = doc.location; let filePath; const isNetwork = doc.location.startsWith('http'); - if (!isNetwork) { - try { - fsPath = resolve(rootDir, doc.location); - } catch {} - filePath = URI.file(fsPath).toString(); - } else { + if (isNetwork) { filePath = this._getTmpProjectPath( projectConfig, true, @@ -818,6 +813,11 @@ export class GraphQLCache { false, 'generated-schema.graphql', ); + } else { + try { + fsPath = resolve(rootDir, doc.location); + } catch {} + filePath = URI.file(fsPath).toString(); } const content = doc.document.loc?.source.body ?? ''; diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index 828b1445d46..fb81b68d07c 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -392,22 +392,16 @@ export class MessageProcessor { return { uri, diagnostics }; } try { - console.log('and here'); const project = this._graphQLCache.getProjectForFile(uri); - console.log('and here 1'); if (project) { - const text = 'text' in textDocument && textDocument.text; + // the disk is always valid here, so the textDocument.text isn't useful I don't think + // const text = 'text' in textDocument && textDocument.text; // for some reason if i try to tell to not parse empty files, it breaks :shrug: // i think this is because if the file change is empty, it doesn't get parsed // TODO: this could be related to a bug in how we are calling didOpenOrSave in our tests // that doesn't reflect the real runtime behavior - const { contents } = await this._parseAndCacheFile( - uri, - project, - // text as string, - ); - console.log('and here 2'); + const { contents } = await this._parseAndCacheFile(uri, project); if (project?.extensions?.languageService?.enableValidation !== false) { await Promise.all( contents.map(async ({ documentString, range }) => { @@ -487,7 +481,6 @@ export class MessageProcessor { if (project?.extensions?.languageService?.enableValidation !== false) { // Send the diagnostics onChange as well try { - console.log({ contents }); await Promise.all( contents.map(async ({ documentString, range }) => { const results = await this._languageService.getDiagnostics( @@ -595,21 +588,17 @@ export class MessageProcessor { // Treat the computed list always complete. const cachedDocument = this._getCachedDocument(textDocument.uri); - console.log({ cachedDocument, uri: textDocument.uri }); if (!cachedDocument) { return { items: [], isIncomplete: false }; } const found = cachedDocument.contents.find(content => { const currentRange = content.range; - console.log({ currentRange, position: toPosition(position) }); if (currentRange?.containsPosition(toPosition(position))) { return true; } }); - console.log({ found }); - // If there is no GraphQL query in this file, return an empty result. if (!found) { return { items: [], isIncomplete: false }; @@ -797,24 +786,17 @@ export class MessageProcessor { const { textDocument, position } = params; const project = this._graphQLCache.getProjectForFile(textDocument.uri); const cachedDocument = this._getCachedDocument(textDocument.uri); - console.log({ cachedDocument }); if (!cachedDocument) { return []; } const found = cachedDocument.contents.find(content => { - console.log(content.range, toPosition(position)); const currentRange = content?.range; - if ( - currentRange && - currentRange?.containsPosition(toPosition(position)) - ) { + if (currentRange?.containsPosition(toPosition(position))) { return true; } }); - console.log({ found }, 'definition'); - // If there is no GraphQL query in this file, return an empty result. if (!found) { return []; @@ -833,9 +815,7 @@ export class MessageProcessor { toPosition(position), textDocument.uri, ); - console.log({ result }); - } catch (err) { - console.error(err); + } catch { // these thrown errors end up getting fired before the service is initialized, so lets cool down on that } @@ -865,7 +845,6 @@ export class MessageProcessor { const vOffset = isEmbedded ? cachedDoc?.contents[0].range?.start.line ?? 0 : parentRange.start.line; - console.log({ defRange }); defRange.setStart( (defRange.start.line += vOffset), @@ -1333,11 +1312,15 @@ export class MessageProcessor { } private _getCachedDocument(uri: string): CachedDocumentType | null { - const fileCache = this._getDocumentCacheForFile(uri); - console.log(fileCache); - const cachedDocument = fileCache?.get(uri); - if (cachedDocument) { - return cachedDocument; + const project = this._graphQLCache.getProjectForFile(uri); + if (project) { + const cachedDocument = this._graphQLCache._getCachedDocument( + uri, + project, + ); + if (cachedDocument) { + return cachedDocument; + } } return null; @@ -1347,7 +1330,6 @@ export class MessageProcessor { uri: Uri, contents: CachedContent[], ): Promise | null> { - console.log('invalidate'); let documentCache = this._getDocumentCacheForFile(uri); if (!documentCache) { const project = await this._graphQLCache.getProjectForFile(uri); @@ -1386,7 +1368,7 @@ export class MessageProcessor { export function processDiagnosticsMessage( results: Diagnostic[], query: string, - range: RangeType | null, + range?: RangeType, ): Diagnostic[] { const queryLines = query.split('\n'); const totalLines = queryLines.length; diff --git a/packages/graphql-language-service/src/types.ts b/packages/graphql-language-service/src/types.ts index 57096358613..3f0817983d0 100644 --- a/packages/graphql-language-service/src/types.ts +++ b/packages/graphql-language-service/src/types.ts @@ -106,6 +106,7 @@ export interface IRange { setStart(line: number, character: number): void; containsPosition(position: IPosition): boolean; } + export type CachedContent = { documentString: string; range?: IRange; diff --git a/yarn.lock b/yarn.lock index d53b5499606..ade5e4da403 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1036,10 +1036,10 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== -"@babel/parser@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" - integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== +"@babel/parser@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" + integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -2392,10 +2392,10 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" - integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== +"@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.22.5", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.7", "@babel/traverse@^7.7.2": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" + integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -2403,8 +2403,8 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" debug "^4.3.1" globals "^11.1.0" @@ -2460,10 +2460,10 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@babel/types@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" - integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== +"@babel/types@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== dependencies: "@babel/helper-string-parser" "^7.23.4" "@babel/helper-validator-identifier" "^7.22.20" @@ -3710,6 +3710,19 @@ "@graphql-tools/utils" "^10.0.0" tslib "^2.4.0" +"@graphql-tools/graphql-tag-pluck@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.0.tgz#11bb8c627253137b39b34fb765cd6ebe506388b9" + integrity sha512-gNqukC+s7iHC7vQZmx1SEJQmLnOguBq+aqE2zV2+o1hxkExvKqyFli1SY/9gmukFIKpKutCIj+8yLOM+jARutw== + dependencies: + "@babel/core" "^7.22.9" + "@babel/parser" "^7.16.8" + "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/traverse" "^7.16.8" + "@babel/types" "^7.16.8" + "@graphql-tools/utils" "^10.0.13" + tslib "^2.4.0" + "@graphql-tools/import@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/import/-/import-7.0.0.tgz#a6a91a90a707d5f46bad0fd3fde2f407b548b2be" From 7839f7969e11acfd84aab564932dec523cda4577 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 17 Mar 2024 10:40:03 +0100 Subject: [PATCH 49/49] only 3 spec errors left! --- .../src/GraphQLCache.ts | 31 +++++++++++++------ .../src/__tests__/MessageProcessor.spec.ts | 10 +++--- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index d02da820712..ccb6dc07235 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -320,7 +320,7 @@ export class GraphQLCache { ast, range: new Range( new Position(0, 0), - new Position(lines.length, lines.at(-1).length), + new Position(lines.length, lines.at(-1)?.length ?? 0), ), }, ], @@ -332,7 +332,7 @@ export class GraphQLCache { projectCache.delete(project.schema.toString()); this._setDefinitionCache( - [{ documentString: text, ast, range: null }], + [{ documentString: text, ast, range: undefined }], this._typeDefinitionsCache.get(projectCacheKey) || new Map(), uri, ); @@ -543,7 +543,20 @@ export class GraphQLCache { if (typeof pointer === 'string') { try { const { fsPath } = URI.parse(pointer); - return this.loadTypeDefs(project, fsPath, target); + // @ts-expect-error these are always here. better typings soon + + return project._extensionsRegistry.loaders[target].loadTypeDefs( + fsPath, + { + cwd: project.dirpath, + includes: project.include, + excludes: project.exclude, + includeSources: true, + assumeValid: false, + noLocation: false, + assumeValidSDL: false, + }, + ); } catch {} } // @ts-expect-error these are always here. better typings soon @@ -802,7 +815,12 @@ export class GraphQLCache { let fsPath = doc.location; let filePath; const isNetwork = doc.location.startsWith('http'); - if (isNetwork) { + if (!isNetwork) { + try { + fsPath = resolve(rootDir, doc.location); + } catch {} + filePath = URI.file(fsPath).toString(); + } else { filePath = this._getTmpProjectPath( projectConfig, true, @@ -813,11 +831,6 @@ export class GraphQLCache { false, 'generated-schema.graphql', ); - } else { - try { - fsPath = resolve(rootDir, doc.location); - } catch {} - filePath = URI.file(fsPath).toString(); } const content = doc.document.loc?.source.body ?? ''; diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index 9da9bee3700..db402abf259 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -310,7 +310,7 @@ describe('the lsp', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); }); - it.only('caches files and schema with a URL config', async () => { + it('caches files and schema with a URL config', async () => { const project = new MockProject({ files: [ ['query.graphql', 'query { test { isTest, ...T } }'], @@ -481,10 +481,10 @@ describe('the lsp', () => { expect(project.lsp._logger.error).not.toHaveBeenCalled(); expect(await project.lsp._graphQLCache.getSchema('a')).toBeDefined(); - // const file = await readFile(join(genSchemaPath.replace('default', 'a')), { - // encoding: 'utf-8', - // }); - // expect(file.split('\n').length).toBeGreaterThan(10); + const file = await readFile(join(genSchemaPath.replace('default', 'a')), { + encoding: 'utf-8', + }); + expect(file.split('\n').length).toBeGreaterThan(10); // add a new typescript file with empty query to the b project // and expect autocomplete to only show options for project b await project.addFile(