Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

svelte support and fix some bugs and add some tests #2829

Merged
merged 1 commit into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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