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
@@ -1,95 +1,63 @@
1
- import path, { extname } from "path";
2
- import { lstatSync, readFileSync } from "fs";
3
1
  import { URI } from "vscode-uri";
4
2
 
5
- import {
6
- TypeSystemDefinitionNode,
7
- isTypeSystemDefinitionNode,
8
- TypeSystemExtensionNode,
9
- isTypeSystemExtensionNode,
10
- DefinitionNode,
11
- GraphQLSchema,
12
- Kind,
13
- } from "graphql";
3
+ import { GraphQLSchema } from "graphql";
14
4
 
15
5
  import {
16
- TextDocument,
17
6
  NotificationHandler,
18
7
  PublishDiagnosticsParams,
19
- Position,
8
+ CancellationToken,
9
+ SymbolInformation,
10
+ Connection,
11
+ ServerRequestHandler,
12
+ TextDocumentChangeEvent,
13
+ StarRequestHandler,
14
+ StarNotificationHandler,
15
+ ServerCapabilities,
20
16
  } from "vscode-languageserver/node";
21
-
22
- import { GraphQLDocument, extractGraphQLDocuments } from "../document";
17
+ import { TextDocument } from "vscode-languageserver-textdocument";
23
18
 
24
19
  import type { LoadingHandler } from "../loadingHandler";
25
20
  import { FileSet } from "../fileSet";
26
- import {
27
- ApolloConfig,
28
- ClientConfig,
29
- isClientConfig,
30
- isLocalServiceConfig,
31
- keyEnvVar,
32
- RoverConfig,
33
- } from "../config";
34
- import {
35
- schemaProviderFromConfig,
36
- GraphQLSchemaProvider,
37
- SchemaResolveConfig,
38
- } from "../providers/schema";
39
- import { ApolloEngineClient, ClientIdentity } from "../engine";
21
+ import { ApolloConfig, ClientConfig, RoverConfig } from "../config";
40
22
  import type { ProjectStats } from "../../messages";
41
23
 
42
24
  export type DocumentUri = string;
43
25
 
44
- const fileAssociations: { [extension: string]: string } = {
45
- ".graphql": "graphql",
46
- ".gql": "graphql",
47
- ".js": "javascript",
48
- ".ts": "typescript",
49
- ".jsx": "javascriptreact",
50
- ".tsx": "typescriptreact",
51
- ".vue": "vue",
52
- ".svelte": "svelte",
53
- ".py": "python",
54
- ".rb": "ruby",
55
- ".dart": "dart",
56
- ".re": "reason",
57
- ".ex": "elixir",
58
- ".exs": "elixir",
59
- };
60
-
61
- interface GraphQLProjectConfig {
62
- clientIdentity: ClientIdentity;
26
+ export interface GraphQLProjectConfig {
63
27
  config: ClientConfig | RoverConfig;
64
28
  configFolderURI: URI;
65
29
  loadingHandler: LoadingHandler;
66
30
  }
67
31
 
68
- export abstract class GraphQLProject implements GraphQLSchemaProvider {
69
- public schemaProvider: GraphQLSchemaProvider;
32
+ type ConnectionHandler = {
33
+ [K in keyof Connection as K extends `on${string}`
34
+ ? K
35
+ : never]: Connection[K] extends (
36
+ params: ServerRequestHandler<any, any, any, any> & infer P,
37
+ token: CancellationToken,
38
+ ) => any
39
+ ? P
40
+ : never;
41
+ };
42
+
43
+ export abstract class GraphQLProject {
70
44
  protected _onDiagnostics?: NotificationHandler<PublishDiagnosticsParams>;
71
45
 
72
46
  private _isReady: boolean;
73
47
  private readyPromise: Promise<void>;
74
- protected engineClient?: ApolloEngineClient;
75
-
76
- private needsValidation = false;
77
-
78
- protected documentsByFile: Map<DocumentUri, GraphQLDocument[]> = new Map();
79
-
80
48
  public config: ApolloConfig;
81
- public schema?: GraphQLSchema;
82
- private fileSet: FileSet;
83
- private rootURI: URI;
49
+ protected schema?: GraphQLSchema;
50
+ protected rootURI: URI;
84
51
  protected loadingHandler: LoadingHandler;
85
52
 
86
53
  protected lastLoadDate?: number;
87
54
 
55
+ private configFileSet: FileSet;
56
+
88
57
  constructor({
89
58
  config,
90
59
  configFolderURI,
91
60
  loadingHandler,
92
- clientIdentity,
93
61
  }: GraphQLProjectConfig) {
94
62
  this.config = config;
95
63
  this.loadingHandler = loadingHandler;
@@ -97,41 +65,25 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
97
65
  // if a config doesn't have a uri associated, we can assume the `rootURI` is the project's root.
98
66
  this.rootURI = config.configDirURI || configFolderURI;
99
67
 
100
- const { includes = [], excludes = [] } = isClientConfig(config)
101
- ? config.client
102
- : {
103
- /** TODO */
104
- };
105
- const fileSet = new FileSet({
68
+ this.configFileSet = new FileSet({
106
69
  rootURI: this.rootURI,
107
70
  includes: [
108
- ...includes,
109
71
  ".env",
110
72
  "apollo.config.js",
111
73
  "apollo.config.cjs",
112
74
  "apollo.config.mjs",
113
75
  "apollo.config.ts",
114
76
  ],
115
- // We do not want to include the local schema file in our list of documents
116
- excludes: [...excludes, ...this.getRelativeLocalSchemaFilePaths()],
117
- configURI: config.configURI,
77
+ excludes: [],
118
78
  });
119
79
 
120
- this.fileSet = fileSet;
121
- this.schemaProvider = schemaProviderFromConfig(config, clientIdentity);
122
- const { engine } = config;
123
- if (engine.apiKey) {
124
- this.engineClient = new ApolloEngineClient(
125
- engine.apiKey!,
126
- engine.endpoint,
127
- clientIdentity,
128
- );
129
- }
130
-
131
80
  this._isReady = false;
132
- // FIXME: Instead of `Promise.all`, we should catch individual promise rejections
133
- // so we can show multiple errors.
134
- this.readyPromise = Promise.all(this.initialize())
81
+ this.readyPromise = Promise.resolve()
82
+ .then(
83
+ // FIXME: Instead of `Promise.all`, we should catch individual promise rejections
84
+ // so we can show multiple errors.
85
+ () => Promise.all(this.initialize()),
86
+ )
135
87
  .then(() => {
136
88
  this._isReady = true;
137
89
  })
@@ -145,7 +97,7 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
145
97
 
146
98
  abstract get displayName(): string;
147
99
 
148
- protected abstract initialize(): Promise<void>[];
100
+ abstract initialize(): Promise<void>[];
149
101
 
150
102
  abstract getProjectStats(): ProjectStats;
151
103
 
@@ -153,15 +105,6 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
153
105
  return this._isReady;
154
106
  }
155
107
 
156
- get engine(): ApolloEngineClient {
157
- // handle error states for missing engine config
158
- // all in the same place :tada:
159
- if (!this.engineClient) {
160
- throw new Error(`Unable to find ${keyEnvVar}`);
161
- }
162
- return this.engineClient!;
163
- }
164
-
165
108
  get whenReady(): Promise<void> {
166
109
  return this.readyPromise;
167
110
  }
@@ -171,236 +114,42 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
171
114
  return this.initialize();
172
115
  }
173
116
 
174
- public resolveSchema(config: SchemaResolveConfig): Promise<GraphQLSchema> {
175
- this.lastLoadDate = +new Date();
176
- return this.schemaProvider.resolveSchema(config);
177
- }
178
-
179
- public resolveFederatedServiceSDL() {
180
- return this.schemaProvider.resolveFederatedServiceSDL();
181
- }
182
-
183
- public onSchemaChange(handler: NotificationHandler<GraphQLSchema>) {
184
- this.lastLoadDate = +new Date();
185
- return this.schemaProvider.onSchemaChange(handler);
186
- }
187
-
188
117
  onDiagnostics(handler: NotificationHandler<PublishDiagnosticsParams>) {
189
118
  this._onDiagnostics = handler;
190
119
  }
191
120
 
192
- includesFile(uri: DocumentUri) {
193
- return this.fileSet.includesFile(uri);
194
- }
195
-
196
- allIncludedFiles() {
197
- return this.fileSet.allFiles();
198
- }
199
-
200
- async scanAllIncludedFiles() {
201
- await this.loadingHandler.handle(
202
- `Loading queries for ${this.displayName}`,
203
- (async () => {
204
- for (const filePath of this.allIncludedFiles()) {
205
- const uri = URI.file(filePath).toString();
206
-
207
- // If we already have query documents for this file, that means it was either
208
- // opened or changed before we got a chance to read it.
209
- if (this.documentsByFile.has(uri)) continue;
210
-
211
- this.fileDidChange(uri);
212
- }
213
- })(),
214
- );
215
- }
216
-
217
- fileDidChange(uri: DocumentUri) {
218
- const filePath = URI.parse(uri).fsPath;
219
- const extension = extname(filePath);
220
- const languageId = fileAssociations[extension];
221
-
222
- // Don't process files of an unsupported filetype
223
- if (!languageId) return;
224
-
225
- // Don't process directories. Directories might be named like files so
226
- // we have to explicitly check.
227
- if (!lstatSync(filePath).isFile()) return;
228
-
229
- const contents = readFileSync(filePath, "utf8");
230
- const document = TextDocument.create(uri, languageId, -1, contents);
231
- this.documentDidChange(document);
232
- }
233
-
234
- fileWasDeleted(uri: DocumentUri) {
235
- this.removeGraphQLDocumentsFor(uri);
236
- this.checkForDuplicateOperations();
121
+ abstract includesFile(uri: DocumentUri, languageId?: string): boolean;
122
+ isConfiguredBy(uri: DocumentUri): boolean {
123
+ return this.configFileSet.includesFile(uri);
237
124
  }
238
125
 
239
- documentDidChange(document: TextDocument) {
240
- const documents = extractGraphQLDocuments(
241
- document,
242
- this.config.client && this.config.client.tagName,
243
- );
244
- if (documents) {
245
- this.documentsByFile.set(document.uri, documents);
246
- this.invalidate();
247
- } else {
248
- this.removeGraphQLDocumentsFor(document.uri);
249
- }
250
- this.checkForDuplicateOperations();
251
- }
252
-
253
- checkForDuplicateOperations(): void {
254
- const filePathForOperationName: Record<string, string> = {};
255
- for (const [fileUri, documentsForFile] of this.documentsByFile.entries()) {
256
- const filePath = URI.parse(fileUri).fsPath;
257
- for (const document of documentsForFile) {
258
- if (!document.ast) continue;
259
- for (const definition of document.ast.definitions) {
260
- if (
261
- definition.kind === Kind.OPERATION_DEFINITION &&
262
- definition.name
263
- ) {
264
- const operationName = definition.name.value;
265
- if (operationName in filePathForOperationName) {
266
- const conflictingFilePath =
267
- filePathForOperationName[operationName];
268
- throw new Error(
269
- `️️There are multiple definitions for the \`${definition.name.value}\` operation. Please fix all naming conflicts before continuing.\nConflicting definitions found at ${filePath} and ${conflictingFilePath}.`,
270
- );
271
- }
272
- filePathForOperationName[operationName] = filePath;
273
- }
274
- }
275
- }
276
- }
277
- }
278
-
279
- private removeGraphQLDocumentsFor(uri: DocumentUri) {
280
- if (this.documentsByFile.has(uri)) {
281
- this.documentsByFile.delete(uri);
282
-
283
- if (this._onDiagnostics) {
284
- this._onDiagnostics({ uri: uri, diagnostics: [] });
285
- }
286
-
287
- this.invalidate();
288
- }
289
- }
290
-
291
- protected invalidate() {
292
- if (!this.needsValidation && this.isReady) {
293
- setTimeout(() => {
294
- this.validateIfNeeded();
295
- }, 0);
296
- this.needsValidation = true;
297
- }
298
- }
299
-
300
- private validateIfNeeded() {
301
- if (!this.needsValidation || !this.isReady) return;
302
-
303
- this.validate();
304
-
305
- this.needsValidation = false;
306
- }
307
-
308
- private getRelativeLocalSchemaFilePaths(): string[] {
309
- const serviceConfig =
310
- isClientConfig(this.config) &&
311
- typeof this.config.client.service === "object" &&
312
- isLocalServiceConfig(this.config.client.service)
313
- ? this.config.client.service
314
- : undefined;
315
- const localSchemaFile = serviceConfig?.localSchemaFile;
316
- return (
317
- localSchemaFile === undefined
318
- ? []
319
- : Array.isArray(localSchemaFile)
320
- ? localSchemaFile
321
- : [localSchemaFile]
322
- ).map((filePath) =>
323
- path.relative(this.rootURI.fsPath, path.join(process.cwd(), filePath)),
324
- );
325
- }
326
-
327
- abstract validate(): void;
328
-
329
- clearAllDiagnostics() {
330
- if (!this._onDiagnostics) return;
126
+ abstract onDidChangeWatchedFiles: ConnectionHandler["onDidChangeWatchedFiles"];
127
+ onDidOpen?: (event: TextDocumentChangeEvent<TextDocument>) => void;
128
+ onDidClose?: (event: TextDocumentChangeEvent<TextDocument>) => void;
129
+ abstract documentDidChange(document: TextDocument): void;
130
+ abstract clearAllDiagnostics(): void;
331
131
 
332
- for (const uri of this.documentsByFile.keys()) {
333
- this._onDiagnostics({ uri, diagnostics: [] });
334
- }
335
- }
132
+ onCompletion?: ConnectionHandler["onCompletion"];
133
+ onHover?: ConnectionHandler["onHover"];
134
+ onDefinition?: ConnectionHandler["onDefinition"];
135
+ onReferences?: ConnectionHandler["onReferences"];
136
+ onDocumentSymbol?: ConnectionHandler["onDocumentSymbol"];
137
+ onCodeLens?: ConnectionHandler["onCodeLens"];
138
+ onCodeAction?: ConnectionHandler["onCodeAction"];
336
139
 
337
- documentsAt(uri: DocumentUri): GraphQLDocument[] | undefined {
338
- return this.documentsByFile.get(uri);
339
- }
140
+ onUnhandledRequest?: StarRequestHandler;
141
+ onUnhandledNotification?: (
142
+ connection: Connection,
143
+ ...rest: Parameters<StarNotificationHandler>
144
+ ) => ReturnType<StarNotificationHandler>;
340
145
 
341
- documentAt(
342
- uri: DocumentUri,
343
- position: Position,
344
- ): GraphQLDocument | undefined {
345
- const queryDocuments = this.documentsByFile.get(uri);
346
- if (!queryDocuments) return undefined;
146
+ dispose?(): void;
347
147
 
348
- return queryDocuments.find((document) =>
349
- document.containsPosition(position),
350
- );
351
- }
352
-
353
- get documents(): GraphQLDocument[] {
354
- const documents: GraphQLDocument[] = [];
355
- for (const documentsForFile of this.documentsByFile.values()) {
356
- documents.push(...documentsForFile);
357
- }
358
- return documents;
359
- }
148
+ provideSymbol?(
149
+ query: string,
150
+ token: CancellationToken,
151
+ ): Promise<SymbolInformation[]>;
360
152
 
361
- get definitions(): DefinitionNode[] {
362
- const definitions = [];
363
-
364
- for (const document of this.documents) {
365
- if (!document.ast) continue;
366
-
367
- definitions.push(...document.ast.definitions);
368
- }
369
-
370
- return definitions;
371
- }
372
-
373
- definitionsAt(uri: DocumentUri): DefinitionNode[] {
374
- const documents = this.documentsAt(uri);
375
- if (!documents) return [];
376
-
377
- const definitions = [];
378
-
379
- for (const document of documents) {
380
- if (!document.ast) continue;
381
-
382
- definitions.push(...document.ast.definitions);
383
- }
384
-
385
- return definitions;
386
- }
387
-
388
- get typeSystemDefinitionsAndExtensions(): (
389
- | TypeSystemDefinitionNode
390
- | TypeSystemExtensionNode
391
- )[] {
392
- const definitionsAndExtensions = [];
393
- for (const document of this.documents) {
394
- if (!document.ast) continue;
395
- for (const definition of document.ast.definitions) {
396
- if (
397
- isTypeSystemDefinitionNode(definition) ||
398
- isTypeSystemExtensionNode(definition)
399
- ) {
400
- definitionsAndExtensions.push(definition);
401
- }
402
- }
403
- }
404
- return definitionsAndExtensions;
405
- }
153
+ onVSCodeConnectionInitialized?(connection: Connection): void;
154
+ validate?(): void;
406
155
  }