vscode-apollo 2.1.0 → 2.2.1
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.
- package/.github/workflows/build-prs.yml +55 -0
- package/.github/workflows/release.yml +1 -1
- package/.gitleaks.toml +10 -3
- package/.vscode/launch.json +1 -0
- package/.vscodeignore +0 -1
- package/CHANGELOG.md +24 -0
- package/package.json +2 -2
- package/sampleWorkspace/httpSchema/apollo.config.ts +2 -0
- package/sampleWorkspace/httpSchema/self-signed.crt +22 -0
- package/sampleWorkspace/httpSchema/self-signed.key +28 -0
- package/src/__e2e__/mockServer.js +37 -11
- package/src/__e2e__/mocks.js +11 -7
- package/src/__e2e__/runTests.js +8 -6
- package/src/language-server/__e2e__/studioGraph.e2e.ts +4 -3
- package/src/language-server/config/__tests__/loadConfig.ts +9 -6
- package/src/language-server/config/config.ts +24 -11
- package/src/language-server/config/loadConfig.ts +2 -1
- package/src/language-server/fileSet.ts +8 -13
- package/src/language-server/project/base.ts +24 -17
- package/src/language-server/project/client.ts +1 -14
- package/src/language-server/project/internal.ts +18 -13
- package/src/language-server/project/rover/DocumentSynchronization.ts +120 -21
- package/src/language-server/project/rover/project.ts +84 -19
- package/src/language-server/providers/schema/endpoint.ts +15 -8
- package/src/language-server/server.ts +70 -48
- package/src/language-server/utilities/languageIdForExtension.ts +39 -0
- package/src/language-server/workspace.ts +27 -3
- package/src/languageServerClient.ts +13 -15
- package/src/tools/utilities/getLanguageInformation.ts +41 -0
- package/src/tools/utilities/languageInformation.ts +41 -0
- package/syntaxes/graphql.json +2 -2
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
TextDocumentChangeEvent,
|
|
13
13
|
StarRequestHandler,
|
|
14
14
|
StarNotificationHandler,
|
|
15
|
+
ServerCapabilities,
|
|
15
16
|
} from "vscode-languageserver/node";
|
|
16
17
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
17
18
|
|
|
@@ -46,12 +47,13 @@ export abstract class GraphQLProject {
|
|
|
46
47
|
private readyPromise: Promise<void>;
|
|
47
48
|
public config: ApolloConfig;
|
|
48
49
|
protected schema?: GraphQLSchema;
|
|
49
|
-
protected fileSet: FileSet;
|
|
50
50
|
protected rootURI: URI;
|
|
51
51
|
protected loadingHandler: LoadingHandler;
|
|
52
52
|
|
|
53
53
|
protected lastLoadDate?: number;
|
|
54
54
|
|
|
55
|
+
private configFileSet: FileSet;
|
|
56
|
+
|
|
55
57
|
constructor({
|
|
56
58
|
config,
|
|
57
59
|
configFolderURI,
|
|
@@ -63,7 +65,7 @@ export abstract class GraphQLProject {
|
|
|
63
65
|
// if a config doesn't have a uri associated, we can assume the `rootURI` is the project's root.
|
|
64
66
|
this.rootURI = config.configDirURI || configFolderURI;
|
|
65
67
|
|
|
66
|
-
this.
|
|
68
|
+
this.configFileSet = new FileSet({
|
|
67
69
|
rootURI: this.rootURI,
|
|
68
70
|
includes: [
|
|
69
71
|
".env",
|
|
@@ -73,7 +75,6 @@ export abstract class GraphQLProject {
|
|
|
73
75
|
"apollo.config.ts",
|
|
74
76
|
],
|
|
75
77
|
excludes: [],
|
|
76
|
-
configURI: config.configURI,
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
this._isReady = false;
|
|
@@ -117,32 +118,38 @@ export abstract class GraphQLProject {
|
|
|
117
118
|
this._onDiagnostics = handler;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
abstract includesFile(uri: DocumentUri): boolean;
|
|
121
|
+
abstract includesFile(uri: DocumentUri, languageId?: string): boolean;
|
|
122
|
+
isConfiguredBy(uri: DocumentUri): boolean {
|
|
123
|
+
return this.configFileSet.includesFile(uri);
|
|
124
|
+
}
|
|
121
125
|
|
|
122
126
|
abstract onDidChangeWatchedFiles: ConnectionHandler["onDidChangeWatchedFiles"];
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
onDidOpen?: (event: TextDocumentChangeEvent<TextDocument>) => void;
|
|
128
|
+
onDidClose?: (event: TextDocumentChangeEvent<TextDocument>) => void;
|
|
125
129
|
abstract documentDidChange(document: TextDocument): void;
|
|
126
130
|
abstract clearAllDiagnostics(): void;
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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"];
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
onUnhandledRequest?: StarRequestHandler;
|
|
141
|
+
onUnhandledNotification?: (
|
|
138
142
|
connection: Connection,
|
|
139
143
|
...rest: Parameters<StarNotificationHandler>
|
|
140
144
|
) => ReturnType<StarNotificationHandler>;
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
dispose?(): void;
|
|
143
147
|
|
|
144
|
-
|
|
148
|
+
provideSymbol?(
|
|
145
149
|
query: string,
|
|
146
150
|
token: CancellationToken,
|
|
147
151
|
): Promise<SymbolInformation[]>;
|
|
152
|
+
|
|
153
|
+
onVSCodeConnectionInitialized?(connection: Connection): void;
|
|
154
|
+
validate?(): void;
|
|
148
155
|
}
|
|
@@ -201,20 +201,7 @@ export class GraphQLClientProject extends GraphQLInternalProject {
|
|
|
201
201
|
super({ config, configFolderURI, loadingHandler, clientIdentity });
|
|
202
202
|
this.serviceID = config.graph;
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
* This function is used in the Array.filter function below it to remove any .env files and config files.
|
|
206
|
-
* If there are 0 files remaining after removing those files, we should warn the user that their config
|
|
207
|
-
* may be wrong. We shouldn't throw an error here, since they could just be initially setting up a project
|
|
208
|
-
* and there's no way to know for sure that there _should_ be files.
|
|
209
|
-
*/
|
|
210
|
-
const filterConfigAndEnvFiles = (path: string) =>
|
|
211
|
-
!(
|
|
212
|
-
path.includes("apollo.config") ||
|
|
213
|
-
path.includes(".env") ||
|
|
214
|
-
(config.configURI && path === config.configURI.fsPath)
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
if (this.allIncludedFiles().filter(filterConfigAndEnvFiles).length === 0) {
|
|
204
|
+
if (this.allIncludedFiles().length === 0) {
|
|
218
205
|
console.warn(
|
|
219
206
|
"⚠️ It looks like there are 0 files associated with this Apollo Project. " +
|
|
220
207
|
"This may be because you don't have any files yet, or your includes/excludes " +
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
import { ApolloEngineClient, ClientIdentity } from "../engine";
|
|
31
31
|
import { GraphQLProject, DocumentUri, GraphQLProjectConfig } from "./base";
|
|
32
32
|
import throttle from "lodash.throttle";
|
|
33
|
+
import { FileSet } from "../fileSet";
|
|
34
|
+
import { getSupportedExtensions } from "../utilities/languageIdForExtension";
|
|
33
35
|
|
|
34
36
|
const fileAssociations: { [extension: string]: string } = {
|
|
35
37
|
".graphql": "graphql",
|
|
@@ -58,6 +60,7 @@ export abstract class GraphQLInternalProject
|
|
|
58
60
|
{
|
|
59
61
|
public schemaProvider: GraphQLSchemaProvider;
|
|
60
62
|
protected engineClient?: ApolloEngineClient;
|
|
63
|
+
private fileSet: FileSet;
|
|
61
64
|
|
|
62
65
|
private needsValidation = false;
|
|
63
66
|
|
|
@@ -70,16 +73,23 @@ export abstract class GraphQLInternalProject
|
|
|
70
73
|
clientIdentity,
|
|
71
74
|
}: GraphQLInternalProjectConfig) {
|
|
72
75
|
super({ config, configFolderURI, loadingHandler });
|
|
73
|
-
const {
|
|
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;
|
|
74
82
|
|
|
75
83
|
this.documentsByFile = new Map();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
});
|
|
83
93
|
|
|
84
94
|
this.schemaProvider = schemaProviderFromConfig(config, clientIdentity);
|
|
85
95
|
const { engine } = config;
|
|
@@ -327,8 +337,6 @@ export abstract class GraphQLInternalProject
|
|
|
327
337
|
}
|
|
328
338
|
return definitionsAndExtensions;
|
|
329
339
|
}
|
|
330
|
-
onDidOpen: undefined;
|
|
331
|
-
onDidClose: undefined;
|
|
332
340
|
onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
|
|
333
341
|
params,
|
|
334
342
|
) => {
|
|
@@ -343,7 +351,4 @@ export abstract class GraphQLInternalProject
|
|
|
343
351
|
}
|
|
344
352
|
}
|
|
345
353
|
};
|
|
346
|
-
onUnhandledRequest: undefined;
|
|
347
|
-
onUnhandledNotification: undefined;
|
|
348
|
-
dispose: undefined;
|
|
349
354
|
}
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
Diagnostic,
|
|
9
9
|
NotificationHandler,
|
|
10
10
|
PublishDiagnosticsParams,
|
|
11
|
+
SemanticTokensRequest,
|
|
12
|
+
ProtocolRequestType,
|
|
13
|
+
SemanticTokensParams,
|
|
14
|
+
SemanticTokens,
|
|
15
|
+
CancellationToken,
|
|
11
16
|
} from "vscode-languageserver-protocol";
|
|
12
17
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
13
18
|
import { DocumentUri, GraphQLProject } from "../base";
|
|
@@ -94,10 +99,17 @@ export class DocumentSynchronization {
|
|
|
94
99
|
private pendingDocumentChanges = new Map<DocumentUri, TextDocument>();
|
|
95
100
|
private knownFiles = new Map<
|
|
96
101
|
DocumentUri,
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
| {
|
|
103
|
+
source: "editor";
|
|
104
|
+
full: TextDocument;
|
|
105
|
+
parts: ReadonlyArray<FilePart>;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
source: "lsp";
|
|
109
|
+
full: Pick<TextDocument, "uri">;
|
|
110
|
+
parts?: undefined;
|
|
111
|
+
diagnostics?: Diagnostic[];
|
|
112
|
+
}
|
|
101
113
|
>();
|
|
102
114
|
|
|
103
115
|
constructor(
|
|
@@ -105,6 +117,11 @@ export class DocumentSynchronization {
|
|
|
105
117
|
type: ProtocolNotificationType<P, RO>,
|
|
106
118
|
params?: P,
|
|
107
119
|
) => Promise<void>,
|
|
120
|
+
private sendRequest: <P, R, PR, E, RO>(
|
|
121
|
+
type: ProtocolRequestType<P, R, PR, E, RO>,
|
|
122
|
+
params: P,
|
|
123
|
+
token?: CancellationToken,
|
|
124
|
+
) => Promise<R>,
|
|
108
125
|
private sendDiagnostics: NotificationHandler<PublishDiagnosticsParams>,
|
|
109
126
|
) {}
|
|
110
127
|
|
|
@@ -148,7 +165,11 @@ export class DocumentSynchronization {
|
|
|
148
165
|
const newObj = Object.fromEntries(
|
|
149
166
|
newParts.map((p) => [p.fractionalIndex, p]),
|
|
150
167
|
);
|
|
151
|
-
this.knownFiles.set(document.uri, {
|
|
168
|
+
this.knownFiles.set(document.uri, {
|
|
169
|
+
source: "editor",
|
|
170
|
+
full: document,
|
|
171
|
+
parts: newParts,
|
|
172
|
+
});
|
|
152
173
|
|
|
153
174
|
for (const newPart of newParts) {
|
|
154
175
|
const previousPart = previousObj[newPart.fractionalIndex];
|
|
@@ -188,7 +209,9 @@ export class DocumentSynchronization {
|
|
|
188
209
|
|
|
189
210
|
async resendAllDocuments() {
|
|
190
211
|
for (const file of this.knownFiles.values()) {
|
|
191
|
-
|
|
212
|
+
if (file.source === "editor") {
|
|
213
|
+
await this.sendDocumentChanges(file.full, []);
|
|
214
|
+
}
|
|
192
215
|
}
|
|
193
216
|
}
|
|
194
217
|
|
|
@@ -198,7 +221,7 @@ export class DocumentSynchronization {
|
|
|
198
221
|
this.documentDidChange(params.document);
|
|
199
222
|
};
|
|
200
223
|
|
|
201
|
-
onDidCloseTextDocument: NonNullable<GraphQLProject["onDidClose"]> = (
|
|
224
|
+
onDidCloseTextDocument: NonNullable<GraphQLProject["onDidClose"]> = async (
|
|
202
225
|
params,
|
|
203
226
|
) => {
|
|
204
227
|
const known = this.knownFiles.get(params.document.uri);
|
|
@@ -206,15 +229,15 @@ export class DocumentSynchronization {
|
|
|
206
229
|
return;
|
|
207
230
|
}
|
|
208
231
|
this.knownFiles.delete(params.document.uri);
|
|
209
|
-
|
|
210
|
-
known.parts
|
|
211
|
-
this.sendNotification(DidCloseTextDocumentNotification.type, {
|
|
232
|
+
if (known.source === "editor") {
|
|
233
|
+
for (const part of known.parts) {
|
|
234
|
+
await this.sendNotification(DidCloseTextDocumentNotification.type, {
|
|
212
235
|
textDocument: {
|
|
213
236
|
uri: getUri(known.full, part),
|
|
214
237
|
},
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
218
241
|
};
|
|
219
242
|
|
|
220
243
|
async documentDidChange(document: TextDocument) {
|
|
@@ -244,7 +267,7 @@ export class DocumentSynchronization {
|
|
|
244
267
|
): Promise<T | undefined> {
|
|
245
268
|
await this.synchronizedWithDocument(positionParams.textDocument.uri);
|
|
246
269
|
const found = this.knownFiles.get(positionParams.textDocument.uri);
|
|
247
|
-
if (!found) {
|
|
270
|
+
if (!found || found.source !== "editor") {
|
|
248
271
|
return;
|
|
249
272
|
}
|
|
250
273
|
const match = findContainedSourceAndPosition(
|
|
@@ -264,11 +287,14 @@ export class DocumentSynchronization {
|
|
|
264
287
|
handlePartDiagnostics(params: PublishDiagnosticsParams) {
|
|
265
288
|
DEBUG && console.log("Received diagnostics", params);
|
|
266
289
|
const uriDetails = splitUri(params.uri);
|
|
267
|
-
if (!uriDetails) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
290
|
const found = this.knownFiles.get(uriDetails.uri);
|
|
271
|
-
if (!found) {
|
|
291
|
+
if (!found || found.source === "lsp") {
|
|
292
|
+
this.knownFiles.set(uriDetails.uri, {
|
|
293
|
+
source: "lsp",
|
|
294
|
+
full: { uri: uriDetails.uri },
|
|
295
|
+
diagnostics: params.diagnostics,
|
|
296
|
+
});
|
|
297
|
+
this.sendDiagnostics(params);
|
|
272
298
|
return;
|
|
273
299
|
}
|
|
274
300
|
const part = found.parts.find(
|
|
@@ -294,15 +320,88 @@ export class DocumentSynchronization {
|
|
|
294
320
|
}
|
|
295
321
|
|
|
296
322
|
get openDocuments() {
|
|
297
|
-
return [...this.knownFiles.values()]
|
|
323
|
+
return [...this.knownFiles.values()]
|
|
324
|
+
.filter((f) => f.source === "editor")
|
|
325
|
+
.map((f) => f.full);
|
|
298
326
|
}
|
|
299
327
|
|
|
300
328
|
clearAllDiagnostics() {
|
|
301
329
|
for (const file of this.knownFiles.values()) {
|
|
302
|
-
|
|
303
|
-
part.
|
|
330
|
+
if (file.source === "editor") {
|
|
331
|
+
for (const part of file.parts) {
|
|
332
|
+
part.diagnostics = [];
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
file.diagnostics = [];
|
|
304
336
|
}
|
|
305
337
|
this.sendDiagnostics({ uri: file.full.uri, diagnostics: [] });
|
|
306
338
|
}
|
|
307
339
|
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Receives semantic tokens for all sub-documents and glues them together.
|
|
343
|
+
* See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens
|
|
344
|
+
* TLDR: The tokens are a flat array of numbers, where each token is represented by 5 numbers.
|
|
345
|
+
* The first two numbers represent the token's delta line and delta start character and might need adjusing
|
|
346
|
+
* relative to the start of a sub-document in relation to the position of the last token of the previous sub-document.
|
|
347
|
+
*
|
|
348
|
+
* There is also an "incremental" version of this request, but we don't support it yet.
|
|
349
|
+
* This is complicated enough as it is.
|
|
350
|
+
*/
|
|
351
|
+
async getFullSemanticTokens(
|
|
352
|
+
params: SemanticTokensParams,
|
|
353
|
+
cancellationToken: CancellationToken,
|
|
354
|
+
): Promise<SemanticTokens | null> {
|
|
355
|
+
await this.synchronizedWithDocument(params.textDocument.uri);
|
|
356
|
+
const found = this.knownFiles.get(params.textDocument.uri);
|
|
357
|
+
if (!found || found.source !== "editor") {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const allParts = await Promise.all(
|
|
361
|
+
found.parts.map(async (part) => {
|
|
362
|
+
return {
|
|
363
|
+
part,
|
|
364
|
+
tokens: await this.sendRequest(
|
|
365
|
+
SemanticTokensRequest.type,
|
|
366
|
+
{
|
|
367
|
+
textDocument: { uri: getUri(found.full, part) },
|
|
368
|
+
},
|
|
369
|
+
cancellationToken,
|
|
370
|
+
),
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
let line = 0,
|
|
375
|
+
char = 0,
|
|
376
|
+
lastLine = 0,
|
|
377
|
+
lastChar = 0;
|
|
378
|
+
const combinedTokens = [];
|
|
379
|
+
for (const { part, tokens } of allParts) {
|
|
380
|
+
if (!tokens) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
line = part.source.locationOffset.line - 1;
|
|
384
|
+
char = part.source.locationOffset.column - 1;
|
|
385
|
+
for (let i = 0; i < tokens.data.length; i += 5) {
|
|
386
|
+
const deltaLine = tokens.data[i],
|
|
387
|
+
deltaStartChar = tokens.data[i + 1];
|
|
388
|
+
|
|
389
|
+
// We need to run this loop fully to correctly calculate the `lastLine` and `lastChar`
|
|
390
|
+
// so for the next incoming tokens, we can adjust the delta correctly.
|
|
391
|
+
line = line + deltaLine;
|
|
392
|
+
char = deltaLine === 0 ? char + deltaStartChar : deltaStartChar;
|
|
393
|
+
// we just need to adjust the deltas only for the first token
|
|
394
|
+
if (i === 0) {
|
|
395
|
+
tokens.data[0] = line - lastLine;
|
|
396
|
+
tokens.data[1] = line === lastLine ? lastChar - char : char;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
combinedTokens.push(...tokens.data);
|
|
400
|
+
lastLine = line;
|
|
401
|
+
lastChar = char;
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
data: combinedTokens,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
308
407
|
}
|
|
@@ -3,7 +3,6 @@ import { DocumentUri, GraphQLProject } from "../base";
|
|
|
3
3
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
4
4
|
import {
|
|
5
5
|
CancellationToken,
|
|
6
|
-
SymbolInformation,
|
|
7
6
|
InitializeRequest,
|
|
8
7
|
StreamMessageReader,
|
|
9
8
|
StreamMessageWriter,
|
|
@@ -19,6 +18,12 @@ import {
|
|
|
19
18
|
PublishDiagnosticsNotification,
|
|
20
19
|
ConnectionError,
|
|
21
20
|
ConnectionErrors,
|
|
21
|
+
SemanticTokensRequest,
|
|
22
|
+
ProtocolRequestType0,
|
|
23
|
+
ServerCapabilities,
|
|
24
|
+
SemanticTokensRegistrationType,
|
|
25
|
+
SemanticTokensOptions,
|
|
26
|
+
SemanticTokensRegistrationOptions,
|
|
22
27
|
} from "vscode-languageserver/node";
|
|
23
28
|
import cp from "node:child_process";
|
|
24
29
|
import { GraphQLProjectConfig } from "../base";
|
|
@@ -26,6 +31,10 @@ import { ApolloConfig, RoverConfig } from "../../config";
|
|
|
26
31
|
import { DocumentSynchronization } from "./DocumentSynchronization";
|
|
27
32
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
33
|
import internal from "node:stream";
|
|
34
|
+
import { VSCodeConnection } from "../../server";
|
|
35
|
+
import { getLanguageIdForExtension } from "../../utilities/languageIdForExtension";
|
|
36
|
+
import { extname } from "node:path";
|
|
37
|
+
import type { FileExtension } from "../../../tools/utilities/languageInformation";
|
|
29
38
|
|
|
30
39
|
export const DEBUG = true;
|
|
31
40
|
|
|
@@ -38,6 +47,10 @@ export interface RoverProjectConfig extends GraphQLProjectConfig {
|
|
|
38
47
|
capabilities: ClientCapabilities;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
const supportedLanguageIds = (
|
|
51
|
+
process.env.APOLLO_ROVER_LANGUAGE_IDS || "graphql"
|
|
52
|
+
).split(",");
|
|
53
|
+
|
|
41
54
|
export class RoverProject extends GraphQLProject {
|
|
42
55
|
config: RoverConfig;
|
|
43
56
|
/**
|
|
@@ -52,11 +65,13 @@ export class RoverProject extends GraphQLProject {
|
|
|
52
65
|
| undefined;
|
|
53
66
|
private disposed = false;
|
|
54
67
|
readonly capabilities: ClientCapabilities;
|
|
68
|
+
roverCapabilities?: ServerCapabilities;
|
|
55
69
|
get displayName(): string {
|
|
56
70
|
return "Rover Project";
|
|
57
71
|
}
|
|
58
72
|
private documents = new DocumentSynchronization(
|
|
59
73
|
this.sendNotification.bind(this),
|
|
74
|
+
this.sendRequest.bind(this),
|
|
60
75
|
(diagnostics) => this._onDiagnostics?.(diagnostics),
|
|
61
76
|
);
|
|
62
77
|
|
|
@@ -143,7 +158,18 @@ export class RoverProject extends GraphQLProject {
|
|
|
143
158
|
"Connection is closed.",
|
|
144
159
|
);
|
|
145
160
|
}
|
|
146
|
-
const
|
|
161
|
+
const args = ["lsp", "--elv2-license", "accept"];
|
|
162
|
+
if (this.config.rover.profile) {
|
|
163
|
+
args.push("--profile", this.config.rover.profile);
|
|
164
|
+
}
|
|
165
|
+
if (this.config.rover.supergraphConfig) {
|
|
166
|
+
args.push("--supergraph-config", this.config.rover.supergraphConfig);
|
|
167
|
+
}
|
|
168
|
+
args.push(...this.config.rover.extraArgs);
|
|
169
|
+
|
|
170
|
+
DEBUG &&
|
|
171
|
+
console.log(`starting ${this.config.rover.bin} '${args.join("' '")}'`);
|
|
172
|
+
const child = cp.spawn(this.config.rover.bin, args, {
|
|
147
173
|
env: DEBUG ? { RUST_BACKTRACE: "1" } : {},
|
|
148
174
|
stdio: ["pipe", "pipe", DEBUG ? "inherit" : "ignore"],
|
|
149
175
|
});
|
|
@@ -185,6 +211,7 @@ export class RoverProject extends GraphQLProject {
|
|
|
185
211
|
},
|
|
186
212
|
source.token,
|
|
187
213
|
);
|
|
214
|
+
this.roverCapabilities = status.capabilities;
|
|
188
215
|
DEBUG && console.log("Connection initialized", status);
|
|
189
216
|
|
|
190
217
|
await this.connectionStorage.run(
|
|
@@ -203,12 +230,16 @@ export class RoverProject extends GraphQLProject {
|
|
|
203
230
|
return { type: "Rover", loaded: true };
|
|
204
231
|
}
|
|
205
232
|
|
|
206
|
-
includesFile(
|
|
207
|
-
|
|
233
|
+
includesFile(
|
|
234
|
+
uri: DocumentUri,
|
|
235
|
+
languageId = getLanguageIdForExtension(extname(uri) as FileExtension),
|
|
236
|
+
) {
|
|
237
|
+
return (
|
|
238
|
+
uri.startsWith(this.rootURI.toString()) &&
|
|
239
|
+
supportedLanguageIds.includes(languageId)
|
|
240
|
+
);
|
|
208
241
|
}
|
|
209
242
|
|
|
210
|
-
validate?: () => void;
|
|
211
|
-
|
|
212
243
|
onDidChangeWatchedFiles: GraphQLProject["onDidChangeWatchedFiles"] = (
|
|
213
244
|
params,
|
|
214
245
|
) => {
|
|
@@ -250,8 +281,17 @@ export class RoverProject extends GraphQLProject {
|
|
|
250
281
|
this.sendRequest(HoverRequest.type, virtualParams, token),
|
|
251
282
|
);
|
|
252
283
|
|
|
253
|
-
onUnhandledRequest: GraphQLProject["onUnhandledRequest"] = (
|
|
254
|
-
|
|
284
|
+
onUnhandledRequest: GraphQLProject["onUnhandledRequest"] = async (
|
|
285
|
+
type,
|
|
286
|
+
params,
|
|
287
|
+
token,
|
|
288
|
+
) => {
|
|
289
|
+
if (isRequestType(SemanticTokensRequest.type, type, params)) {
|
|
290
|
+
return this.documents.getFullSemanticTokens(params, token);
|
|
291
|
+
} else {
|
|
292
|
+
DEBUG && console.info("unhandled request from VSCode", { type, params });
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
255
295
|
};
|
|
256
296
|
onUnhandledNotification: GraphQLProject["onUnhandledNotification"] = (
|
|
257
297
|
_connection,
|
|
@@ -262,15 +302,40 @@ export class RoverProject extends GraphQLProject {
|
|
|
262
302
|
console.info("unhandled notification from VSCode", { type, params });
|
|
263
303
|
};
|
|
264
304
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
305
|
+
async onVSCodeConnectionInitialized(connection: VSCodeConnection) {
|
|
306
|
+
// Report the actual capabilities of the upstream LSP to VSCode.
|
|
307
|
+
// It is important to actually "ask" the LSP for this, because the capabilities
|
|
308
|
+
// also define the semantic token legend, which is needed to interpret the tokens.
|
|
309
|
+
await this.getConnection();
|
|
310
|
+
const capabilities = this.roverCapabilities;
|
|
311
|
+
if (capabilities?.semanticTokensProvider) {
|
|
312
|
+
connection.client.register(SemanticTokensRegistrationType.type, {
|
|
313
|
+
documentSelector: null,
|
|
314
|
+
...capabilities.semanticTokensProvider,
|
|
315
|
+
full: {
|
|
316
|
+
// the upstream LSP supports "true" here, but we don't yet
|
|
317
|
+
delta: false,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isRequestType<R, PR, E, RO>(
|
|
325
|
+
type: ProtocolRequestType0<R, PR, E, RO>,
|
|
326
|
+
method: string,
|
|
327
|
+
params: any,
|
|
328
|
+
): params is PR;
|
|
329
|
+
function isRequestType<P, R, PR, E, RO>(
|
|
330
|
+
type: ProtocolRequestType<P, R, PR, E, RO>,
|
|
331
|
+
method: string,
|
|
332
|
+
params: any,
|
|
333
|
+
): params is P;
|
|
334
|
+
function isRequestType(
|
|
335
|
+
type:
|
|
336
|
+
| ProtocolRequestType0<any, any, any, any>
|
|
337
|
+
| ProtocolRequestType<any, any, any, any, any>,
|
|
338
|
+
method: string,
|
|
339
|
+
) {
|
|
340
|
+
return type.method === method;
|
|
276
341
|
}
|
|
@@ -17,7 +17,16 @@ import { RemoteServiceConfig } from "../../config";
|
|
|
17
17
|
import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base";
|
|
18
18
|
import { Debug } from "../../utilities";
|
|
19
19
|
import { isString } from "util";
|
|
20
|
-
|
|
20
|
+
import { fetch as undiciFetch, Agent } from "undici";
|
|
21
|
+
|
|
22
|
+
const skipSSLValidationFetchOptions = {
|
|
23
|
+
// see https://github.com/nodejs/undici/issues/1489#issuecomment-1543856261
|
|
24
|
+
dispatcher: new Agent({
|
|
25
|
+
connect: {
|
|
26
|
+
rejectUnauthorized: false,
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
} satisfies import("undici").RequestInit;
|
|
21
30
|
export class EndpointSchemaProvider implements GraphQLSchemaProvider {
|
|
22
31
|
private schema?: GraphQLSchema;
|
|
23
32
|
private federatedServiceSDL?: string;
|
|
@@ -28,11 +37,11 @@ export class EndpointSchemaProvider implements GraphQLSchemaProvider {
|
|
|
28
37
|
const { skipSSLValidation, url, headers } = this.config;
|
|
29
38
|
const options: HttpOptions = {
|
|
30
39
|
uri: url,
|
|
40
|
+
fetch: undiciFetch as typeof fetch,
|
|
31
41
|
};
|
|
42
|
+
|
|
32
43
|
if (url.startsWith("https:") && skipSSLValidation) {
|
|
33
|
-
options.fetchOptions =
|
|
34
|
-
agent: new HTTPSAgent({ rejectUnauthorized: false }),
|
|
35
|
-
};
|
|
44
|
+
options.fetchOptions = skipSSLValidationFetchOptions;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
const { data, errors } = (await toPromise(
|
|
@@ -92,12 +101,10 @@ export class EndpointSchemaProvider implements GraphQLSchemaProvider {
|
|
|
92
101
|
const { skipSSLValidation, url, headers } = this.config;
|
|
93
102
|
const options: HttpOptions = {
|
|
94
103
|
uri: url,
|
|
95
|
-
fetch,
|
|
104
|
+
fetch: undiciFetch as typeof fetch,
|
|
96
105
|
};
|
|
97
106
|
if (url.startsWith("https:") && skipSSLValidation) {
|
|
98
|
-
options.fetchOptions =
|
|
99
|
-
agent: new HTTPSAgent({ rejectUnauthorized: false }),
|
|
100
|
-
};
|
|
107
|
+
options.fetchOptions = skipSSLValidationFetchOptions;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
const getFederationInfoQuery = `
|