diff --git a/.changeset/clean-carpets-fetch.md b/.changeset/clean-carpets-fetch.md new file mode 100644 index 00000000000..e58d46452cb --- /dev/null +++ b/.changeset/clean-carpets-fetch.md @@ -0,0 +1,6 @@ +--- +'graphql-language-service-server': patch +'vscode-graphql': patch +--- + +major bugfixes with `onDidChange` and `onDidChangeWatchedFiles` events diff --git a/.changeset/good-comics-divide.md b/.changeset/good-comics-divide.md new file mode 100644 index 00000000000..ae474c37fed --- /dev/null +++ b/.changeset/good-comics-divide.md @@ -0,0 +1,9 @@ +--- +'graphql-language-service-server': patch +'vscode-graphql': patch +'graphql-language-service-server': patch +'graphql-language-service-server-cli': patch +--- + +svelte language support, using the vue sfc parser introduced for vue support + diff --git a/packages/graphql-language-service-server/README.md b/packages/graphql-language-service-server/README.md index 27981131343..ce4c5baac38 100644 --- a/packages/graphql-language-service-server/README.md +++ b/packages/graphql-language-service-server/README.md @@ -20,7 +20,7 @@ Supported features include: - Autocomplete suggestions (**spec-compliant**) - Hyperlink to fragment definitions and named types (type, input, enum) definitions (**spec-compliant**) - Outline view support for queries -- Support for `gql` `graphql` and other template tags inside javascript, typescript, jsx and tsx files, and an interface to allow custom parsing of all files. +- Support for `gql` `graphql` and other template tags inside javascript, typescript, jsx, ts, vue and svelte files, and an interface to allow custom parsing of all files. ## Installation and Usage diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json index 9419860553f..0b3faa9b429 100644 --- a/packages/graphql-language-service-server/package.json +++ b/packages/graphql-language-service-server/package.json @@ -25,7 +25,10 @@ "keywords": [ "graphql", "language server", - "LSP" + "LSP", + "vue", + "svelte", + "typescript" ], "main": "dist/index.js", "module": "esm/index.js", diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index af8676e31f5..754cf5788f1 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -264,35 +264,34 @@ export class MessageProcessor { this._isGraphQLConfigMissing = true; this._logConfigError(err.message); - return; } else if (err instanceof ProjectNotFoundError) { + // 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', ); } else if (err instanceof ConfigInvalidError) { + this._isGraphQLConfigMissing = true; this._logConfigError(`Invalid configuration\n${err.message}`); } else if (err instanceof ConfigEmptyError) { + this._isGraphQLConfigMissing = true; this._logConfigError(err.message); } else if (err instanceof LoaderNoResultError) { + this._isGraphQLConfigMissing = true; this._logConfigError(err.message); + return; } else { + // if it's another kind of error, + // lets just assume the config is missing and + // disable language features + this._isGraphQLConfigMissing = true; this._logConfigError( // @ts-expect-error err?.message ?? err?.toString(), ); } - // force a no-op for all other language feature requests. - // - // TODO: contextually disable language features based on whether config is present - // Ideally could do this when sending initialization message, but extension settings are not available - // then, which are needed in order to initialize the language server (graphql-config loadConfig settings, for example) - this._isInitialized = false; - - // set this to false here so that if we don't have a missing config file issue anymore - // we can keep re-trying to load the config, so that on the next add or save event, - // it can keep retrying the language service - this._isGraphQLConfigMissing = false; + return; } _logConfigError(errorMessage: string) { @@ -350,21 +349,27 @@ export class MessageProcessor { await this._invalidateCache(textDocument, uri, contents); } else { - const configMatchers = [ - 'graphql.config', - 'graphqlrc', - 'package.json', - this._settings.load?.fileName, - ].filter(Boolean); - if (configMatchers.some(v => uri.match(v)?.length)) { + const configMatchers = ['graphql.config', 'graphqlrc'].filter(Boolean); + if (this._settings?.load?.fileName) { + configMatchers.push(this._settings.load.fileName); + } + + const hasGraphQLConfigFile = configMatchers.some( + v => uri.match(v)?.length, + ); + const hasPackageGraphQLConfig = + uri.match('package.json')?.length && require(uri)?.graphql; + if (hasGraphQLConfigFile || hasPackageGraphQLConfig) { this._logger.info('updating graphql config'); this._updateGraphQLConfig(); return { uri, diagnostics: [] }; - } - // update graphql config only when graphql config is saved! - const cachedDocument = this._getCachedDocument(textDocument.uri); - if (cachedDocument) { - contents = cachedDocument.contents; + } else { + // update graphql config only when graphql config is saved! + const cachedDocument = this._getCachedDocument(textDocument.uri); + if (cachedDocument) { + contents = cachedDocument.contents; + } + return null; } } if (!this._graphQLCache) { @@ -411,7 +416,11 @@ export class MessageProcessor { async handleDidChangeNotification( params: DidChangeTextDocumentParams, ): Promise { - if (!this._isInitialized || !this._graphQLCache) { + if ( + this._isGraphQLConfigMissing || + !this._isInitialized || + !this._graphQLCache + ) { return null; } // For every `textDocument/didChange` event, keep a cache of textDocuments @@ -428,61 +437,65 @@ export class MessageProcessor { '`textDocument`, `textDocument.uri`, and `contentChanges` arguments are required.', ); } - const textDocument = params.textDocument; - const contentChanges = params.contentChanges; - const contentChange = contentChanges[contentChanges.length - 1]; - - // As `contentChanges` is an array and we just want the - // latest update to the text, grab the last entry from the array. const uri = textDocument.uri; + const project = this._graphQLCache.getProjectForFile(uri); + try { + const contentChanges = params.contentChanges; + const contentChange = contentChanges[contentChanges.length - 1]; - // 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); + // As `contentChanges` is an array and we just want the + // latest update to the text, grab the last entry from the array. - const cachedDocument = this._getCachedDocument(uri); + // 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); - if (!cachedDocument) { - return null; - } + const cachedDocument = this._getCachedDocument(uri); - await this._updateFragmentDefinition(uri, contents); - await this._updateObjectTypeDefinition(uri, contents); + if (!cachedDocument) { + return null; + } - const project = this._graphQLCache.getProjectForFile(uri); - const diagnostics: Diagnostic[] = []; + await this._updateFragmentDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents); - if (project?.extensions?.languageService?.enableValidation !== false) { - // Send the diagnostics onChange as well - await Promise.all( - contents.map(async ({ query, range }) => { - const results = await this._languageService.getDiagnostics( - query, - uri, - this._isRelayCompatMode(query), - ); - if (results && results.length > 0) { - diagnostics.push( - ...processDiagnosticsMessage(results, query, range), + const diagnostics: Diagnostic[] = []; + + if (project?.extensions?.languageService?.enableValidation !== false) { + // Send the diagnostics onChange as well + await Promise.all( + contents.map(async ({ query, range }) => { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), ); - } + if (results && results.length > 0) { + diagnostics.push( + ...processDiagnosticsMessage(results, query, range), + ); + } + }), + ); + } + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didChange', + projectName: project?.name, + fileName: uri, }), ); - } - - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'textDocument/didChange', - projectName: project?.name, - fileName: uri, - }), - ); - return { uri, diagnostics }; + return { uri, diagnostics }; + } catch (err) { + this._handleConfigError({ err, uri }); + return { uri, diagnostics: [] }; + } } async handleDidChangeConfiguration( _params: DidChangeConfigurationParams, @@ -653,14 +666,23 @@ export class MessageProcessor { async handleWatchedFilesChangedNotification( params: DidChangeWatchedFilesParams, ): Promise | null> { - if (!this._isInitialized || !this._graphQLCache) { + if ( + this._isGraphQLConfigMissing || + !this._isInitialized || + !this._graphQLCache + ) { return null; } return Promise.all( params.changes.map(async (change: FileEvent) => { - if (!this._isInitialized || !this._graphQLCache) { - throw Error('No cache available for handleWatchedFilesChanged'); + if ( + this._isGraphQLConfigMissing || + !this._isInitialized || + !this._graphQLCache + ) { + this._logger.warn('No cache available for handleWatchedFilesChanged'); + return; } else if ( change.type === FileChangeTypeKind.Created || change.type === FileChangeTypeKind.Changed 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 debe9ea6162..5d8a6580ed1 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts @@ -88,7 +88,7 @@ describe('GraphQLCache', () => { expect(schema instanceof GraphQLSchema).toEqual(true); }); - it.skip('generates the schema correctly from endpoint', async () => { + it('generates the schema correctly from endpoint', async () => { const introspectionResult = { data: introspectionFromSchema( await graphQLRC.getProject('testWithSchema').getSchema(), 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 98cb5e2e3ae..7a7b0890c8a 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -738,4 +738,49 @@ query Test { expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); }); }); + + describe('handleWatchedFilesChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = jest.requireMock('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('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(); + }); + }); }); 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 2c58d285134..5ee43c192ed 100644 --- a/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts @@ -213,6 +213,19 @@ export default defineComponent({ query {id}`); }); + it('finds queries in tagged templates in Svelte using normal +`; + const contents = findGraphQLTags(text, '.svelte'); + expect(contents[0].template).toEqual(` +query {id}`); + }); + it('finds multiple queries in a single file', async () => { const text = `something({ else: () => gql\` query {} \` diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.ts b/packages/graphql-language-service-server/src/findGraphQLTags.ts index 891522ddf9d..b2cc8da7d96 100644 --- a/packages/graphql-language-service-server/src/findGraphQLTags.ts +++ b/packages/graphql-language-service-server/src/findGraphQLTags.ts @@ -101,11 +101,11 @@ export function findGraphQLTags( const plugins = BABEL_PLUGINS.slice(0, BABEL_PLUGINS.length); - const isVue = ext === '.vue'; + const isVueLike = ext === '.vue' || ext === '.svelte'; let parsedASTs: { [key: string]: any }[] = []; - if (isVue) { + if (isVueLike) { const parseVueSFCResult = parseVueSFC(text); if (parseVueSFCResult.type === 'error') { logger.error( diff --git a/packages/vscode-graphql-syntax/README.md b/packages/vscode-graphql-syntax/README.md index f3afd4a6a65..a615388143c 100644 --- a/packages/vscode-graphql-syntax/README.md +++ b/packages/vscode-graphql-syntax/README.md @@ -3,7 +3,7 @@ Adds full GraphQL syntax highlighting and language support such as bracket matching. - Supports `.graphql`/`.gql`/`.graphqls` highlighting -- [Javascript, Typescript & JSX/TSX](#ts) & Vue +- [Javascript, Typescript & JSX/TSX](#ts) & Vue & Svelte - ReasonML/ReScript (`%graphql()` ) - Python - PHP diff --git a/packages/vscode-graphql/src/extension.ts b/packages/vscode-graphql/src/extension.ts index 3790d1147eb..ae143540a41 100644 --- a/packages/vscode-graphql/src/extension.ts +++ b/packages/vscode-graphql/src/extension.ts @@ -74,7 +74,7 @@ export async function activate(context: ExtensionContext) { // TODO: load ignore // These ignore node_modules and .git by default workspace.createFileSystemWatcher( - '**/{*.graphql,*.graphqls,*.gql,*.js,*.mjs,*.cjs,*.esm,*.es,*.es6,*.jsx,*.ts,*.tsx,*.vue}', + '**/{*.graphql,*.graphqls,*.gql,*.js,*.mjs,*.cjs,*.esm,*.es,*.es6,*.jsx,*.ts,*.tsx,*.vue,*.svelte}', ), ], }, diff --git a/packages/vscode-graphql/src/server/index.ts b/packages/vscode-graphql/src/server/index.ts index 153026cbbf4..c03f4c989e3 100644 --- a/packages/vscode-graphql/src/server/index.ts +++ b/packages/vscode-graphql/src/server/index.ts @@ -1,3 +1,6 @@ +// this lives in the same monorepo! most errors you see in +// vscode that aren't highlighting or bracket completion +// related are coming from our LSP server import { startServer } from 'graphql-language-service-server'; // The npm scripts are configured to only build this once before