Skip to content

Commit

Permalink
svelte support and fix some bugs and add some tests (#2829)
Browse files Browse the repository at this point in the history
  • Loading branch information
acao committed Oct 22, 2022
1 parent c33f2a2 commit c835ca8
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 78 deletions.
6 changes: 6 additions & 0 deletions .changeset/clean-carpets-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'graphql-language-service-server': patch
'vscode-graphql': patch
---

major bugfixes with `onDidChange` and `onDidChangeWatchedFiles` events
9 changes: 9 additions & 0 deletions .changeset/good-comics-divide.md
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion packages/graphql-language-service-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion packages/graphql-language-service-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"keywords": [
"graphql",
"language server",
"LSP"
"LSP",
"vue",
"svelte",
"typescript"
],
"main": "dist/index.js",
"module": "esm/index.js",
Expand Down
164 changes: 93 additions & 71 deletions packages/graphql-language-service-server/src/MessageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -411,7 +416,11 @@ export class MessageProcessor {
async handleDidChangeNotification(
params: DidChangeTextDocumentParams,
): Promise<PublishDiagnosticsParams | null> {
if (!this._isInitialized || !this._graphQLCache) {
if (
this._isGraphQLConfigMissing ||
!this._isInitialized ||
!this._graphQLCache
) {
return null;
}
// For every `textDocument/didChange` event, keep a cache of textDocuments
Expand All @@ -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,
Expand Down Expand Up @@ -653,14 +666,23 @@ export class MessageProcessor {
async handleWatchedFilesChangedNotification(
params: DidChangeWatchedFilesParams,
): Promise<Array<PublishDiagnosticsParams | undefined> | 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,19 @@ export default defineComponent({
query {id}`);
});

it('finds queries in tagged templates in Svelte using normal <script>', async () => {
const text = `
<script>
gql\`
query {id}
\`;
</script>
`;
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 {} \`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-graphql-syntax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-graphql/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
),
],
},
Expand Down
3 changes: 3 additions & 0 deletions packages/vscode-graphql/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit c835ca8

Please sign in to comment.