vscode-apollo 2.0.1 → 2.1.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 (37) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.vscode/launch.json +4 -1
  3. package/CHANGELOG.md +27 -0
  4. package/package.json +9 -3
  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 +19 -10
  22. package/src/language-server/config/config.ts +26 -1
  23. package/src/language-server/config/which.d.ts +19 -0
  24. package/src/language-server/document.ts +86 -53
  25. package/src/language-server/fileSet.ts +7 -0
  26. package/src/language-server/project/base.ts +58 -316
  27. package/src/language-server/project/client.ts +730 -7
  28. package/src/language-server/project/internal.ts +349 -0
  29. package/src/language-server/project/rover/DocumentSynchronization.ts +308 -0
  30. package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
  31. package/src/language-server/project/rover/project.ts +276 -0
  32. package/src/language-server/server.ts +129 -62
  33. package/src/language-server/utilities/__tests__/source.test.ts +162 -0
  34. package/src/language-server/utilities/source.ts +38 -3
  35. package/src/language-server/workspace.ts +34 -9
  36. package/syntaxes/graphql.js.json +18 -21
  37. package/src/language-server/languageProvider.ts +0 -795
@@ -1,86 +1,53 @@
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,
20
15
  } from "vscode-languageserver/node";
21
-
22
- import { GraphQLDocument, extractGraphQLDocuments } from "../document";
16
+ import { TextDocument } from "vscode-languageserver-textdocument";
23
17
 
24
18
  import type { LoadingHandler } from "../loadingHandler";
25
19
  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";
20
+ import { ApolloConfig, ClientConfig, RoverConfig } from "../config";
40
21
  import type { ProjectStats } from "../../messages";
41
22
 
42
23
  export type DocumentUri = string;
43
24
 
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;
25
+ export interface GraphQLProjectConfig {
63
26
  config: ClientConfig | RoverConfig;
64
27
  configFolderURI: URI;
65
28
  loadingHandler: LoadingHandler;
66
29
  }
67
30
 
68
- export abstract class GraphQLProject implements GraphQLSchemaProvider {
69
- public schemaProvider: GraphQLSchemaProvider;
31
+ type ConnectionHandler = {
32
+ [K in keyof Connection as K extends `on${string}`
33
+ ? K
34
+ : never]: Connection[K] extends (
35
+ params: ServerRequestHandler<any, any, any, any> & infer P,
36
+ token: CancellationToken,
37
+ ) => any
38
+ ? P
39
+ : never;
40
+ };
41
+
42
+ export abstract class GraphQLProject {
70
43
  protected _onDiagnostics?: NotificationHandler<PublishDiagnosticsParams>;
71
44
 
72
45
  private _isReady: boolean;
73
46
  private readyPromise: Promise<void>;
74
- protected engineClient?: ApolloEngineClient;
75
-
76
- private needsValidation = false;
77
-
78
- protected documentsByFile: Map<DocumentUri, GraphQLDocument[]> = new Map();
79
-
80
47
  public config: ApolloConfig;
81
- public schema?: GraphQLSchema;
82
- private fileSet: FileSet;
83
- private rootURI: URI;
48
+ protected schema?: GraphQLSchema;
49
+ protected fileSet: FileSet;
50
+ protected rootURI: URI;
84
51
  protected loadingHandler: LoadingHandler;
85
52
 
86
53
  protected lastLoadDate?: number;
@@ -89,7 +56,6 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
89
56
  config,
90
57
  configFolderURI,
91
58
  loadingHandler,
92
- clientIdentity,
93
59
  }: GraphQLProjectConfig) {
94
60
  this.config = config;
95
61
  this.loadingHandler = loadingHandler;
@@ -97,41 +63,26 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
97
63
  // if a config doesn't have a uri associated, we can assume the `rootURI` is the project's root.
98
64
  this.rootURI = config.configDirURI || configFolderURI;
99
65
 
100
- const { includes = [], excludes = [] } = isClientConfig(config)
101
- ? config.client
102
- : {
103
- /** TODO */
104
- };
105
- const fileSet = new FileSet({
66
+ this.fileSet = new FileSet({
106
67
  rootURI: this.rootURI,
107
68
  includes: [
108
- ...includes,
109
69
  ".env",
110
70
  "apollo.config.js",
111
71
  "apollo.config.cjs",
112
72
  "apollo.config.mjs",
113
73
  "apollo.config.ts",
114
74
  ],
115
- // We do not want to include the local schema file in our list of documents
116
- excludes: [...excludes, ...this.getRelativeLocalSchemaFilePaths()],
75
+ excludes: [],
117
76
  configURI: config.configURI,
118
77
  });
119
78
 
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
79
  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())
80
+ this.readyPromise = Promise.resolve()
81
+ .then(
82
+ // FIXME: Instead of `Promise.all`, we should catch individual promise rejections
83
+ // so we can show multiple errors.
84
+ () => Promise.all(this.initialize()),
85
+ )
135
86
  .then(() => {
136
87
  this._isReady = true;
137
88
  })
@@ -145,7 +96,7 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
145
96
 
146
97
  abstract get displayName(): string;
147
98
 
148
- protected abstract initialize(): Promise<void>[];
99
+ abstract initialize(): Promise<void>[];
149
100
 
150
101
  abstract getProjectStats(): ProjectStats;
151
102
 
@@ -153,15 +104,6 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
153
104
  return this._isReady;
154
105
  }
155
106
 
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
107
  get whenReady(): Promise<void> {
166
108
  return this.readyPromise;
167
109
  }
@@ -171,236 +113,36 @@ export abstract class GraphQLProject implements GraphQLSchemaProvider {
171
113
  return this.initialize();
172
114
  }
173
115
 
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
116
  onDiagnostics(handler: NotificationHandler<PublishDiagnosticsParams>) {
189
117
  this._onDiagnostics = handler;
190
118
  }
191
119
 
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
- }
120
+ abstract includesFile(uri: DocumentUri): boolean;
216
121
 
217
- fileDidChange(uri: DocumentUri) {
218
- const filePath = URI.parse(uri).fsPath;
219
- const extension = extname(filePath);
220
- const languageId = fileAssociations[extension];
122
+ abstract onDidChangeWatchedFiles: ConnectionHandler["onDidChangeWatchedFiles"];
123
+ abstract onDidOpen?: (event: TextDocumentChangeEvent<TextDocument>) => void;
124
+ abstract onDidClose?: (event: TextDocumentChangeEvent<TextDocument>) => void;
125
+ abstract documentDidChange(document: TextDocument): void;
126
+ abstract clearAllDiagnostics(): void;
221
127
 
222
- // Don't process files of an unsupported filetype
223
- if (!languageId) return;
128
+ abstract onCompletion?: ConnectionHandler["onCompletion"];
129
+ abstract onHover?: ConnectionHandler["onHover"];
130
+ abstract onDefinition?: ConnectionHandler["onDefinition"];
131
+ abstract onReferences?: ConnectionHandler["onReferences"];
132
+ abstract onDocumentSymbol?: ConnectionHandler["onDocumentSymbol"];
133
+ abstract onCodeLens?: ConnectionHandler["onCodeLens"];
134
+ abstract onCodeAction?: ConnectionHandler["onCodeAction"];
224
135
 
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();
237
- }
238
-
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
- }
136
+ abstract onUnhandledRequest?: StarRequestHandler;
137
+ abstract onUnhandledNotification?: (
138
+ connection: Connection,
139
+ ...rest: Parameters<StarNotificationHandler>
140
+ ) => ReturnType<StarNotificationHandler>;
278
141
 
279
- private removeGraphQLDocumentsFor(uri: DocumentUri) {
280
- if (this.documentsByFile.has(uri)) {
281
- this.documentsByFile.delete(uri);
142
+ abstract dispose?(): void;
282
143
 
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;
331
-
332
- for (const uri of this.documentsByFile.keys()) {
333
- this._onDiagnostics({ uri, diagnostics: [] });
334
- }
335
- }
336
-
337
- documentsAt(uri: DocumentUri): GraphQLDocument[] | undefined {
338
- return this.documentsByFile.get(uri);
339
- }
340
-
341
- documentAt(
342
- uri: DocumentUri,
343
- position: Position,
344
- ): GraphQLDocument | undefined {
345
- const queryDocuments = this.documentsByFile.get(uri);
346
- if (!queryDocuments) return undefined;
347
-
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
- }
360
-
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
- }
144
+ abstract provideSymbol?(
145
+ query: string,
146
+ token: CancellationToken,
147
+ ): Promise<SymbolInformation[]>;
406
148
  }