vscode-apollo 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.vscode/launch.json +5 -1
  3. package/CHANGELOG.md +41 -0
  4. package/package.json +9 -4
  5. package/renovate.json +2 -1
  6. package/sampleWorkspace/localSchema/src/test.js +3 -0
  7. package/sampleWorkspace/rover/apollo.config.js +3 -0
  8. package/sampleWorkspace/rover/src/test.graphql +14 -0
  9. package/sampleWorkspace/rover/src/test.js +30 -0
  10. package/sampleWorkspace/sampleWorkspace.code-workspace +25 -19
  11. package/src/language-server/__tests__/document.test.ts +161 -3
  12. package/src/language-server/__tests__/fixtures/TypeScript.tmLanguage.json +5749 -0
  13. package/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts +41 -0
  14. package/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts.snap +185 -0
  15. package/src/language-server/__tests__/fixtures/documents/functionCall.ts +93 -0
  16. package/src/language-server/__tests__/fixtures/documents/functionCall.ts.snap +431 -0
  17. package/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts +80 -0
  18. package/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts.snap +353 -0
  19. package/src/language-server/__tests__/fixtures/documents/templateWithComment.ts +38 -0
  20. package/src/language-server/__tests__/fixtures/documents/templateWithComment.ts.snap +123 -0
  21. package/src/language-server/config/__tests__/loadConfig.ts +28 -16
  22. package/src/language-server/config/config.ts +50 -12
  23. package/src/language-server/config/loadConfig.ts +2 -1
  24. package/src/language-server/config/which.d.ts +19 -0
  25. package/src/language-server/document.ts +86 -53
  26. package/src/language-server/fileSet.ts +8 -6
  27. package/src/language-server/project/base.ts +64 -315
  28. package/src/language-server/project/client.ts +731 -21
  29. package/src/language-server/project/internal.ts +354 -0
  30. package/src/language-server/project/rover/DocumentSynchronization.ts +385 -0
  31. package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
  32. package/src/language-server/project/rover/project.ts +341 -0
  33. package/src/language-server/server.ts +187 -98
  34. package/src/language-server/utilities/__tests__/source.test.ts +162 -0
  35. package/src/language-server/utilities/languageIdForExtension.ts +39 -0
  36. package/src/language-server/utilities/source.ts +38 -3
  37. package/src/language-server/workspace.ts +61 -12
  38. package/src/languageServerClient.ts +13 -15
  39. package/src/tools/utilities/getLanguageInformation.ts +41 -0
  40. package/src/tools/utilities/languageInformation.ts +41 -0
  41. package/syntaxes/graphql.js.json +18 -21
  42. package/src/language-server/languageProvider.ts +0 -795
@@ -0,0 +1,354 @@
1
+ import path, { extname } from "path";
2
+ import { lstatSync, readFileSync } from "fs";
3
+ import { URI } from "vscode-uri";
4
+
5
+ import {
6
+ TypeSystemDefinitionNode,
7
+ isTypeSystemDefinitionNode,
8
+ TypeSystemExtensionNode,
9
+ isTypeSystemExtensionNode,
10
+ DefinitionNode,
11
+ GraphQLSchema,
12
+ Kind,
13
+ } from "graphql";
14
+
15
+ import {
16
+ FileChangeType,
17
+ NotificationHandler,
18
+ Position,
19
+ } from "vscode-languageserver/node";
20
+ import { TextDocument } from "vscode-languageserver-textdocument";
21
+
22
+ import { GraphQLDocument, extractGraphQLDocuments } from "../document";
23
+
24
+ import { ClientConfig, isClientConfig, isLocalServiceConfig } from "../config";
25
+ import {
26
+ schemaProviderFromConfig,
27
+ GraphQLSchemaProvider,
28
+ SchemaResolveConfig,
29
+ } from "../providers/schema";
30
+ import { ApolloEngineClient, ClientIdentity } from "../engine";
31
+ import { GraphQLProject, DocumentUri, GraphQLProjectConfig } from "./base";
32
+ import throttle from "lodash.throttle";
33
+ import { FileSet } from "../fileSet";
34
+ import { getSupportedExtensions } from "../utilities/languageIdForExtension";
35
+
36
+ const fileAssociations: { [extension: string]: string } = {
37
+ ".graphql": "graphql",
38
+ ".gql": "graphql",
39
+ ".js": "javascript",
40
+ ".ts": "typescript",
41
+ ".jsx": "javascriptreact",
42
+ ".tsx": "typescriptreact",
43
+ ".vue": "vue",
44
+ ".svelte": "svelte",
45
+ ".py": "python",
46
+ ".rb": "ruby",
47
+ ".dart": "dart",
48
+ ".re": "reason",
49
+ ".ex": "elixir",
50
+ ".exs": "elixir",
51
+ };
52
+
53
+ export interface GraphQLInternalProjectConfig extends GraphQLProjectConfig {
54
+ config: ClientConfig;
55
+ clientIdentity: ClientIdentity;
56
+ }
57
+ export abstract class GraphQLInternalProject
58
+ extends GraphQLProject
59
+ implements GraphQLSchemaProvider
60
+ {
61
+ public schemaProvider: GraphQLSchemaProvider;
62
+ protected engineClient?: ApolloEngineClient;
63
+ private fileSet: FileSet;
64
+
65
+ private needsValidation = false;
66
+
67
+ protected documentsByFile: Map<DocumentUri, GraphQLDocument[]>;
68
+
69
+ constructor({
70
+ config,
71
+ configFolderURI,
72
+ loadingHandler,
73
+ clientIdentity,
74
+ }: GraphQLInternalProjectConfig) {
75
+ super({ config, configFolderURI, loadingHandler });
76
+ const {
77
+ // something like
78
+ // 'src/**/*{.gql,.graphql,.graphqls,.js,.mjs,.cjs,.es6,.pac,.ts,.mts,.cts,.jsx,.tsx,.vue,.svelte,.py,.rpy,.pyw,.cpy,.gyp,.gypi,.pyi,.ipy,.pyt,.rb,.rbx,.rjs,.gemspec,.rake,.ru,.erb,.podspec,.rbi,.dart,.re,.ex,.exs}'
79
+ includes = [`src/**/*{${getSupportedExtensions().join(",")}}`],
80
+ excludes = [],
81
+ } = config.client;
82
+
83
+ this.documentsByFile = new Map();
84
+ this.fileSet = new FileSet({
85
+ rootURI: this.rootURI,
86
+ includes,
87
+ excludes: [
88
+ ...excludes,
89
+ // We do not want to include the local schema file in our list of documents
90
+ ...this.getRelativeLocalSchemaFilePaths(),
91
+ ],
92
+ });
93
+
94
+ this.schemaProvider = schemaProviderFromConfig(config, clientIdentity);
95
+ const { engine } = config;
96
+ if (engine.apiKey) {
97
+ this.engineClient = new ApolloEngineClient(
98
+ engine.apiKey!,
99
+ engine.endpoint,
100
+ clientIdentity,
101
+ );
102
+ }
103
+ }
104
+
105
+ public resolveSchema(config: SchemaResolveConfig): Promise<GraphQLSchema> {
106
+ this.lastLoadDate = +new Date();
107
+ return this.schemaProvider.resolveSchema(config);
108
+ }
109
+
110
+ public resolveFederatedServiceSDL() {
111
+ return this.schemaProvider.resolveFederatedServiceSDL();
112
+ }
113
+
114
+ public onSchemaChange(handler: NotificationHandler<GraphQLSchema>) {
115
+ this.lastLoadDate = +new Date();
116
+ return this.schemaProvider.onSchemaChange(handler);
117
+ }
118
+
119
+ includesFile(uri: DocumentUri) {
120
+ return this.fileSet.includesFile(uri);
121
+ }
122
+
123
+ allIncludedFiles() {
124
+ return this.fileSet.allFiles();
125
+ }
126
+
127
+ async scanAllIncludedFiles() {
128
+ await this.loadingHandler.handle(
129
+ `Loading queries for ${this.displayName}`,
130
+ (async () => {
131
+ for (const filePath of this.allIncludedFiles()) {
132
+ const uri = URI.file(filePath).toString();
133
+
134
+ // If we already have query documents for this file, that means it was either
135
+ // opened or changed before we got a chance to read it.
136
+ if (this.documentsByFile.has(uri)) continue;
137
+
138
+ this.fileDidChange(uri);
139
+ }
140
+ })(),
141
+ );
142
+ }
143
+
144
+ fileDidChange(uri: DocumentUri) {
145
+ const filePath = URI.parse(uri).fsPath;
146
+ const extension = extname(filePath);
147
+ const languageId = fileAssociations[extension];
148
+
149
+ // Don't process files of an unsupported filetype
150
+ if (!languageId) return;
151
+
152
+ // Don't process directories. Directories might be named like files so
153
+ // we have to explicitly check.
154
+ if (!lstatSync(filePath).isFile()) return;
155
+
156
+ const contents = readFileSync(filePath, "utf8");
157
+ const document = TextDocument.create(uri, languageId, -1, contents);
158
+ this.documentDidChange(document);
159
+ }
160
+
161
+ fileWasDeleted(uri: DocumentUri) {
162
+ this.removeGraphQLDocumentsFor(uri);
163
+ this.checkForDuplicateOperations();
164
+ }
165
+
166
+ documentDidChange = (document: TextDocument) => {
167
+ const documents = extractGraphQLDocuments(
168
+ document,
169
+ this.config.client && this.config.client.tagName,
170
+ );
171
+ if (documents) {
172
+ this.documentsByFile.set(document.uri, documents);
173
+ this.invalidate();
174
+ } else {
175
+ this.removeGraphQLDocumentsFor(document.uri);
176
+ }
177
+ this.checkForDuplicateOperations();
178
+ };
179
+
180
+ checkForDuplicateOperations = throttle(
181
+ () => {
182
+ const filePathForOperationName: Record<string, string> = {};
183
+ for (const [
184
+ fileUri,
185
+ documentsForFile,
186
+ ] of this.documentsByFile.entries()) {
187
+ const filePath = URI.parse(fileUri).fsPath;
188
+ for (const document of documentsForFile) {
189
+ if (!document.ast) continue;
190
+ for (const definition of document.ast.definitions) {
191
+ if (
192
+ definition.kind === Kind.OPERATION_DEFINITION &&
193
+ definition.name
194
+ ) {
195
+ const operationName = definition.name.value;
196
+ if (operationName in filePathForOperationName) {
197
+ const conflictingFilePath =
198
+ filePathForOperationName[operationName];
199
+ throw new Error(
200
+ `️️There are multiple definitions for the \`${definition.name.value}\` operation. Please fix all naming conflicts before continuing.\nConflicting definitions found at ${filePath} and ${conflictingFilePath}.`,
201
+ );
202
+ }
203
+ filePathForOperationName[operationName] = filePath;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ },
209
+ 250,
210
+ { leading: true, trailing: true },
211
+ );
212
+
213
+ private removeGraphQLDocumentsFor(uri: DocumentUri) {
214
+ if (this.documentsByFile.has(uri)) {
215
+ this.documentsByFile.delete(uri);
216
+
217
+ if (this._onDiagnostics) {
218
+ this._onDiagnostics({ uri: uri, diagnostics: [] });
219
+ }
220
+
221
+ this.invalidate();
222
+ }
223
+ }
224
+
225
+ protected invalidate() {
226
+ if (!this.needsValidation && this.isReady) {
227
+ setTimeout(() => {
228
+ this.validateIfNeeded();
229
+ }, 0);
230
+ this.needsValidation = true;
231
+ }
232
+ }
233
+
234
+ private validateIfNeeded() {
235
+ if (!this.needsValidation || !this.isReady) return;
236
+
237
+ this.validate();
238
+
239
+ this.needsValidation = false;
240
+ }
241
+
242
+ private getRelativeLocalSchemaFilePaths(): string[] {
243
+ const serviceConfig =
244
+ isClientConfig(this.config) &&
245
+ typeof this.config.client.service === "object" &&
246
+ isLocalServiceConfig(this.config.client.service)
247
+ ? this.config.client.service
248
+ : undefined;
249
+ const localSchemaFile = serviceConfig?.localSchemaFile;
250
+ return (
251
+ localSchemaFile === undefined
252
+ ? []
253
+ : Array.isArray(localSchemaFile)
254
+ ? localSchemaFile
255
+ : [localSchemaFile]
256
+ ).map((filePath) =>
257
+ path.relative(this.rootURI.fsPath, path.join(process.cwd(), filePath)),
258
+ );
259
+ }
260
+
261
+ abstract validate(): void;
262
+
263
+ clearAllDiagnostics() {
264
+ if (!this._onDiagnostics) return;
265
+
266
+ for (const uri of this.documentsByFile.keys()) {
267
+ this._onDiagnostics({ uri, diagnostics: [] });
268
+ }
269
+ }
270
+
271
+ documentsAt(uri: DocumentUri): GraphQLDocument[] | undefined {
272
+ return this.documentsByFile.get(uri);
273
+ }
274
+
275
+ documentAt(
276
+ uri: DocumentUri,
277
+ position: Position,
278
+ ): GraphQLDocument | undefined {
279
+ const queryDocuments = this.documentsByFile.get(uri);
280
+ if (!queryDocuments) return undefined;
281
+
282
+ return queryDocuments.find((document) =>
283
+ document.containsPosition(position),
284
+ );
285
+ }
286
+
287
+ get documents(): GraphQLDocument[] {
288
+ const documents: GraphQLDocument[] = [];
289
+ for (const documentsForFile of this.documentsByFile.values()) {
290
+ documents.push(...documentsForFile);
291
+ }
292
+ return documents;
293
+ }
294
+
295
+ get definitions(): DefinitionNode[] {
296
+ const definitions = [];
297
+
298
+ for (const document of this.documents) {
299
+ if (!document.ast) continue;
300
+
301
+ definitions.push(...document.ast.definitions);
302
+ }
303
+
304
+ return definitions;
305
+ }
306
+
307
+ definitionsAt(uri: DocumentUri): DefinitionNode[] {
308
+ const documents = this.documentsAt(uri);
309
+ if (!documents) return [];
310
+
311
+ const definitions = [];
312
+
313
+ for (const document of documents) {
314
+ if (!document.ast) continue;
315
+
316
+ definitions.push(...document.ast.definitions);
317
+ }
318
+
319
+ return definitions;
320
+ }
321
+
322
+ get typeSystemDefinitionsAndExtensions(): (
323
+ | TypeSystemDefinitionNode
324
+ | TypeSystemExtensionNode
325
+ )[] {
326
+ const definitionsAndExtensions = [];
327
+ for (const document of this.documents) {
328
+ if (!document.ast) continue;
329
+ for (const definition of document.ast.definitions) {
330
+ if (
331
+ isTypeSystemDefinitionNode(definition) ||
332
+ isTypeSystemExtensionNode(definition)
333
+ ) {
334
+ definitionsAndExtensions.push(definition);
335
+ }
336
+ }
337
+ }
338
+ return definitionsAndExtensions;
339
+ }
340
+ onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
341
+ params,
342
+ ) => {
343
+ for (const { uri, type } of params.changes) {
344
+ switch (type) {
345
+ case FileChangeType.Created:
346
+ this.fileDidChange(uri);
347
+ break;
348
+ case FileChangeType.Deleted:
349
+ this.fileWasDeleted(uri);
350
+ break;
351
+ }
352
+ }
353
+ };
354
+ }