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.
Files changed (31) hide show
  1. package/.github/workflows/build-prs.yml +55 -0
  2. package/.github/workflows/release.yml +1 -1
  3. package/.gitleaks.toml +10 -3
  4. package/.vscode/launch.json +1 -0
  5. package/.vscodeignore +0 -1
  6. package/CHANGELOG.md +24 -0
  7. package/package.json +2 -2
  8. package/sampleWorkspace/httpSchema/apollo.config.ts +2 -0
  9. package/sampleWorkspace/httpSchema/self-signed.crt +22 -0
  10. package/sampleWorkspace/httpSchema/self-signed.key +28 -0
  11. package/src/__e2e__/mockServer.js +37 -11
  12. package/src/__e2e__/mocks.js +11 -7
  13. package/src/__e2e__/runTests.js +8 -6
  14. package/src/language-server/__e2e__/studioGraph.e2e.ts +4 -3
  15. package/src/language-server/config/__tests__/loadConfig.ts +9 -6
  16. package/src/language-server/config/config.ts +24 -11
  17. package/src/language-server/config/loadConfig.ts +2 -1
  18. package/src/language-server/fileSet.ts +8 -13
  19. package/src/language-server/project/base.ts +24 -17
  20. package/src/language-server/project/client.ts +1 -14
  21. package/src/language-server/project/internal.ts +18 -13
  22. package/src/language-server/project/rover/DocumentSynchronization.ts +120 -21
  23. package/src/language-server/project/rover/project.ts +84 -19
  24. package/src/language-server/providers/schema/endpoint.ts +15 -8
  25. package/src/language-server/server.ts +70 -48
  26. package/src/language-server/utilities/languageIdForExtension.ts +39 -0
  27. package/src/language-server/workspace.ts +27 -3
  28. package/src/languageServerClient.ts +13 -15
  29. package/src/tools/utilities/getLanguageInformation.ts +41 -0
  30. package/src/tools/utilities/languageInformation.ts +41 -0
  31. package/syntaxes/graphql.json +2 -2
@@ -4,7 +4,6 @@ import {
4
4
  ProposedFeatures,
5
5
  TextDocuments,
6
6
  FileChangeType,
7
- ServerCapabilities,
8
7
  TextDocumentSyncKind,
9
8
  SymbolInformation,
10
9
  FileEvent,
@@ -21,10 +20,16 @@ import {
21
20
  LanguageServerRequests as Requests,
22
21
  } from "../messages";
23
22
  import { isValidationError } from "zod-validation-error";
24
- import { Trie } from "@wry/trie";
25
23
  import { GraphQLProject } from "./project/base";
24
+ import type { LanguageIdExtensionMap } from "../tools/utilities/languageInformation";
25
+ import { setLanguageIdExtensionMap } from "./utilities/languageIdForExtension";
26
+
27
+ export type InitializationOptions = {
28
+ languageIdExtensionMap: LanguageIdExtensionMap;
29
+ };
26
30
 
27
31
  const connection = createConnection(ProposedFeatures.all);
32
+ export type VSCodeConnection = typeof connection;
28
33
 
29
34
  Debug.SetConnection(connection);
30
35
  const { sendNotification: originalSendNotification } = connection;
@@ -37,8 +42,8 @@ connection.sendNotification = async (...args: [any, ...any[]]) => {
37
42
  let hasWorkspaceFolderCapability = false;
38
43
 
39
44
  // Awaitable promise for sending messages before the connection is initialized
40
- let initializeConnection: () => void;
41
- const whenConnectionInitialized: Promise<void> = new Promise(
45
+ let initializeConnection: (c: typeof connection) => void;
46
+ const whenConnectionInitialized: Promise<typeof connection> = new Promise(
42
47
  (resolve) => (initializeConnection = resolve),
43
48
  );
44
49
 
@@ -55,6 +60,7 @@ const workspace = new GraphQLWorkspace(
55
60
  require("../../package.json").version,
56
61
  },
57
62
  },
63
+ whenConnectionInitialized,
58
64
  );
59
65
 
60
66
  workspace.onDiagnostics((params) => {
@@ -84,46 +90,52 @@ workspace.onConfigFilesFound(async (params) => {
84
90
  );
85
91
  });
86
92
 
87
- connection.onInitialize(async ({ capabilities, workspaceFolders }) => {
88
- hasWorkspaceFolderCapability = !!(
89
- capabilities.workspace && capabilities.workspace.workspaceFolders
90
- );
91
- workspace.capabilities = capabilities;
92
-
93
- if (workspaceFolders) {
94
- // We wait until all projects are added, because after `initialize` returns we can get additional requests
95
- // like `textDocument/codeLens`, and that way these can await `GraphQLProject#whenReady` to make sure
96
- // we provide them eventually.
97
- await Promise.all(
98
- workspaceFolders.map((folder) => workspace.addProjectsInFolder(folder)),
93
+ connection.onInitialize(
94
+ async ({ capabilities, workspaceFolders, initializationOptions }) => {
95
+ const { languageIdExtensionMap } =
96
+ initializationOptions as InitializationOptions;
97
+ setLanguageIdExtensionMap(languageIdExtensionMap);
98
+
99
+ hasWorkspaceFolderCapability = !!(
100
+ capabilities.workspace && capabilities.workspace.workspaceFolders
99
101
  );
100
- }
102
+ workspace.capabilities = capabilities;
103
+
104
+ if (workspaceFolders) {
105
+ // We wait until all projects are added, because after `initialize` returns we can get additional requests
106
+ // like `textDocument/codeLens`, and that way these can await `GraphQLProject#whenReady` to make sure
107
+ // we provide them eventually.
108
+ await Promise.all(
109
+ workspaceFolders.map((folder) => workspace.addProjectsInFolder(folder)),
110
+ );
111
+ }
101
112
 
102
- return {
103
- capabilities: {
104
- hoverProvider: true,
105
- completionProvider: {
106
- resolveProvider: false,
107
- triggerCharacters: ["...", "@"],
108
- },
109
- definitionProvider: true,
110
- referencesProvider: true,
111
- documentSymbolProvider: true,
112
- workspaceSymbolProvider: true,
113
- codeLensProvider: {
114
- resolveProvider: false,
113
+ return {
114
+ capabilities: {
115
+ hoverProvider: true,
116
+ completionProvider: {
117
+ resolveProvider: false,
118
+ triggerCharacters: ["...", "@"],
119
+ },
120
+ definitionProvider: true,
121
+ referencesProvider: true,
122
+ documentSymbolProvider: true,
123
+ workspaceSymbolProvider: true,
124
+ codeLensProvider: {
125
+ resolveProvider: false,
126
+ },
127
+ codeActionProvider: true,
128
+ executeCommandProvider: {
129
+ commands: [],
130
+ },
131
+ textDocumentSync: TextDocumentSyncKind.Full,
115
132
  },
116
- codeActionProvider: true,
117
- executeCommandProvider: {
118
- commands: [],
119
- },
120
- textDocumentSync: TextDocumentSyncKind.Full,
121
- } as ServerCapabilities,
122
- };
123
- });
133
+ };
134
+ },
135
+ );
124
136
 
125
137
  connection.onInitialized(async () => {
126
- initializeConnection();
138
+ initializeConnection(connection);
127
139
  if (hasWorkspaceFolderCapability) {
128
140
  connection.workspace.onDidChangeWorkspaceFolders(async (event) => {
129
141
  await Promise.all([
@@ -147,7 +159,10 @@ function isFile(uri: string) {
147
159
  }
148
160
 
149
161
  documents.onDidChangeContent((params) => {
150
- const project = workspace.projectForFile(params.document.uri);
162
+ const project = workspace.projectForFile(
163
+ params.document.uri,
164
+ params.document.languageId,
165
+ );
151
166
  if (!project) return;
152
167
 
153
168
  // Only watch changes to files
@@ -160,12 +175,16 @@ documents.onDidChangeContent((params) => {
160
175
 
161
176
  documents.onDidOpen(
162
177
  (params) =>
163
- workspace.projectForFile(params.document.uri)?.onDidOpen?.(params),
178
+ workspace
179
+ .projectForFile(params.document.uri, params.document.languageId)
180
+ ?.onDidOpen?.(params),
164
181
  );
165
182
 
166
183
  documents.onDidClose(
167
184
  (params) =>
168
- workspace.projectForFile(params.document.uri)?.onDidClose?.(params),
185
+ workspace
186
+ .projectForFile(params.document.uri, params.document.languageId)
187
+ ?.onDidClose?.(params),
169
188
  );
170
189
 
171
190
  connection.onDidChangeWatchedFiles((params) => {
@@ -246,28 +265,31 @@ connection.onWorkspaceSymbol(async (params, token) => {
246
265
 
247
266
  connection.onCompletion(
248
267
  debounceHandler(
249
- (params, token, workDoneProgress, resultProgress) =>
268
+ ((params, token, workDoneProgress, resultProgress) =>
250
269
  workspace
251
270
  .projectForFile(params.textDocument.uri)
252
- ?.onCompletion?.(params, token, workDoneProgress, resultProgress) ?? [],
271
+ ?.onCompletion?.(params, token, workDoneProgress, resultProgress) ??
272
+ []) satisfies Parameters<typeof connection.onCompletion>[0],
253
273
  ),
254
274
  );
255
275
 
256
276
  connection.onCodeLens(
257
277
  debounceHandler(
258
- (params, token, workDoneProgress, resultProgress) =>
278
+ ((params, token, workDoneProgress, resultProgress) =>
259
279
  workspace
260
280
  .projectForFile(params.textDocument.uri)
261
- ?.onCodeLens?.(params, token, workDoneProgress, resultProgress) ?? [],
281
+ ?.onCodeLens?.(params, token, workDoneProgress, resultProgress) ??
282
+ []) satisfies Parameters<typeof connection.onCodeLens>[0],
262
283
  ),
263
284
  );
264
285
 
265
286
  connection.onCodeAction(
266
287
  debounceHandler(
267
- (params, token, workDoneProgress, resultProgress) =>
288
+ ((params, token, workDoneProgress, resultProgress) =>
268
289
  workspace
269
290
  .projectForFile(params.textDocument.uri)
270
- ?.onCodeAction?.(params, token, workDoneProgress, resultProgress) ?? [],
291
+ ?.onCodeAction?.(params, token, workDoneProgress, resultProgress) ??
292
+ []) satisfies Parameters<typeof connection.onCodeAction>[0],
271
293
  ),
272
294
  );
273
295
 
@@ -0,0 +1,39 @@
1
+ import {
2
+ FileExtension,
3
+ LanguageIdExtensionMap,
4
+ supportedLanguageIds,
5
+ } from "../../tools/utilities/languageInformation";
6
+
7
+ let languageIdPerExtension: Record<FileExtension, string> | undefined;
8
+ let supportedExtensions: FileExtension[] | undefined;
9
+
10
+ export function setLanguageIdExtensionMap(map: LanguageIdExtensionMap) {
11
+ languageIdPerExtension = Object.fromEntries(
12
+ Object.entries(map).flatMap(([languageId, extensions]) =>
13
+ extensions.map((extension) => [extension, languageId]),
14
+ ),
15
+ );
16
+ supportedExtensions = supportedLanguageIds.flatMap(
17
+ (languageId) => map[languageId],
18
+ );
19
+ }
20
+
21
+ /**
22
+ * @throws if called before the language server has received options via `onInitialize`.
23
+ */
24
+ export function getLanguageIdForExtension(ext: FileExtension) {
25
+ if (!languageIdPerExtension) {
26
+ throw new Error("LanguageIdExtensionMap not set");
27
+ }
28
+ return languageIdPerExtension[ext];
29
+ }
30
+
31
+ /**
32
+ * @throws if called before the language server has received options via `onInitialize`.
33
+ */
34
+ export function getSupportedExtensions() {
35
+ if (!supportedExtensions) {
36
+ throw new Error("LanguageIdExtensionMap not set");
37
+ }
38
+ return supportedExtensions;
39
+ }
@@ -17,6 +17,7 @@ import { Debug } from "./utilities";
17
17
  import type { EngineDecoration } from "../messages";
18
18
  import { equal } from "@wry/equality";
19
19
  import { isRoverConfig, RoverProject } from "./project/rover/project";
20
+ import { VSCodeConnection } from "./server";
20
21
 
21
22
  export interface WorkspaceConfig {
22
23
  clientIdentity: ClientIdentity;
@@ -35,6 +36,7 @@ export class GraphQLWorkspace {
35
36
  constructor(
36
37
  private LanguageServerLoadingHandler: LanguageServerLoadingHandler,
37
38
  private config: WorkspaceConfig,
39
+ private whenConnectionInitialized: Promise<VSCodeConnection>,
38
40
  ) {}
39
41
 
40
42
  onDiagnostics(handler: NotificationHandler<PublishDiagnosticsParams>) {
@@ -98,6 +100,11 @@ export class GraphQLWorkspace {
98
100
  // base class which is used by codegen and other tools
99
101
  project.whenReady.then(() => project.validate?.());
100
102
 
103
+ if (project.onVSCodeConnectionInitialized) {
104
+ this.whenConnectionInitialized.then(
105
+ project.onVSCodeConnectionInitialized.bind(project),
106
+ );
107
+ }
101
108
  return project;
102
109
  }
103
110
 
@@ -201,7 +208,7 @@ export class GraphQLWorkspace {
201
208
  error = e;
202
209
  }
203
210
 
204
- const project = this.projectForFile(configUri);
211
+ const project = this.projectForConfigFile(configUri);
205
212
 
206
213
  if (this._onConfigFilesFound) {
207
214
  this._onConfigFilesFound([config || error]);
@@ -261,14 +268,31 @@ export class GraphQLWorkspace {
261
268
  return Array.from(this.projectsByFolderUri.values()).flat();
262
269
  }
263
270
 
264
- projectForFile(uri: DocumentUri): GraphQLProject | undefined {
271
+ projectForConfigFile(configUri: DocumentUri): GraphQLProject | undefined {
272
+ for (const projects of this.projectsByFolderUri.values()) {
273
+ const project = projects.find((project) =>
274
+ project.isConfiguredBy(configUri),
275
+ );
276
+ if (project) {
277
+ return project;
278
+ }
279
+ }
280
+ return undefined;
281
+ }
282
+
283
+ projectForFile(
284
+ uri: DocumentUri,
285
+ languageId?: string,
286
+ ): GraphQLProject | undefined {
265
287
  const cachedResult = this._projectForFileCache.get(uri);
266
288
  if (cachedResult) {
267
289
  return cachedResult;
268
290
  }
269
291
 
270
292
  for (const projects of this.projectsByFolderUri.values()) {
271
- const project = projects.find((project) => project.includesFile(uri));
293
+ const project = projects.find((project) =>
294
+ project.includesFile(uri, languageId),
295
+ );
272
296
  if (project) {
273
297
  this._projectForFileCache.set(uri, project);
274
298
  return project;
@@ -6,9 +6,17 @@ import {
6
6
  RevealOutputChannelOn,
7
7
  } from "vscode-languageclient/node";
8
8
  import { workspace, OutputChannel } from "vscode";
9
+ import { supportedLanguageIds } from "./tools/utilities/languageInformation";
10
+ import type { InitializationOptions } from "./language-server/server";
11
+ import { getLangugageInformation } from "./tools/utilities/getLanguageInformation";
9
12
 
10
13
  const { version, referenceID } = require("../package.json");
11
14
 
15
+ const languageIdExtensionMap = getLangugageInformation();
16
+ const supportedExtensions = supportedLanguageIds.flatMap(
17
+ (id) => languageIdExtensionMap[id],
18
+ );
19
+
12
20
  export function getLanguageServerClient(
13
21
  serverModule: string,
14
22
  outputChannel: OutputChannel,
@@ -41,25 +49,12 @@ export function getLanguageServerClient(
41
49
  };
42
50
 
43
51
  const clientOptions: LanguageClientOptions = {
44
- documentSelector: [
45
- "graphql",
46
- "javascript",
47
- "typescript",
48
- "javascriptreact",
49
- "typescriptreact",
50
- "vue",
51
- "svelte",
52
- "python",
53
- "ruby",
54
- "dart",
55
- "reason",
56
- "elixir",
57
- ],
52
+ documentSelector: supportedLanguageIds,
58
53
  synchronize: {
59
54
  fileEvents: [
60
55
  workspace.createFileSystemWatcher("**/.env?(.local)"),
61
56
  workspace.createFileSystemWatcher(
62
- "**/*.{graphql,js,ts,cjs,mjs,jsx,tsx,vue,svelte,py,rb,dart,re,ex,exs}",
57
+ "**/*{" + supportedExtensions.join(",") + "}",
63
58
  ),
64
59
  ],
65
60
  },
@@ -69,6 +64,9 @@ export function getLanguageServerClient(
69
64
  .get("debug.revealOutputOnLanguageServerError")
70
65
  ? RevealOutputChannelOn.Error
71
66
  : RevealOutputChannelOn.Never,
67
+ initializationOptions: {
68
+ languageIdExtensionMap,
69
+ } satisfies InitializationOptions,
72
70
  };
73
71
 
74
72
  return new LanguageClient(
@@ -0,0 +1,41 @@
1
+ import * as vscode from "vscode";
2
+ import {
3
+ LanguageIdExtensionMap,
4
+ minimumKnownExtensions,
5
+ } from "./languageInformation";
6
+
7
+ /**
8
+ * @returns An object with language identifiers as keys and file extensions as values.
9
+ * see https://github.com/microsoft/vscode/issues/109919
10
+ */
11
+ export function getLangugageInformation(): LanguageIdExtensionMap {
12
+ const allKnownExtensions = vscode.extensions.all
13
+ .map(
14
+ (i) =>
15
+ i.packageJSON?.contributes?.languages as (
16
+ | undefined
17
+ | {
18
+ id?: string;
19
+ extensions?: string[];
20
+ }
21
+ )[],
22
+ )
23
+ .flat()
24
+ .filter(
25
+ (i): i is { id: string; extensions: `.${string}`[] } =>
26
+ !!(i && i.id && i.extensions?.length),
27
+ )
28
+ .reduce<Record<string, Set<`.${string}`>>>(
29
+ (acc, i) => {
30
+ if (!acc[i.id]) acc[i.id] = new Set();
31
+ for (const ext of i.extensions) acc[i.id].add(ext);
32
+ return acc;
33
+ },
34
+ Object.fromEntries(
35
+ Object.entries(minimumKnownExtensions).map(([k, v]) => [k, new Set(v)]),
36
+ ),
37
+ );
38
+ return Object.fromEntries(
39
+ Object.entries(allKnownExtensions).map(([k, v]) => [k, [...v]] as const),
40
+ ) as LanguageIdExtensionMap;
41
+ }
@@ -0,0 +1,41 @@
1
+ const _supportedDocumentTypes = [
2
+ "graphql",
3
+ "javascript",
4
+ "typescript",
5
+ "javascriptreact",
6
+ "typescriptreact",
7
+ "vue",
8
+ "svelte",
9
+ "python",
10
+ "ruby",
11
+ "dart",
12
+ "reason",
13
+ "elixir",
14
+ ] as const;
15
+ export type SupportedLanguageIds = (typeof _supportedDocumentTypes)[number];
16
+ export const supportedLanguageIds =
17
+ // remove the `readonly` we get from using `as const`
18
+ _supportedDocumentTypes as any as SupportedLanguageIds[];
19
+
20
+ export type FileExtension = `.${string}`;
21
+
22
+ export const minimumKnownExtensions: Record<
23
+ SupportedLanguageIds,
24
+ FileExtension[]
25
+ > = {
26
+ graphql: [".gql", ".graphql", ".graphqls"],
27
+ javascript: [".js", ".mjs", ".cjs"],
28
+ typescript: [".ts", ".mts", ".cts"],
29
+ javascriptreact: [".jsx"],
30
+ typescriptreact: [".tsx"],
31
+ vue: [".vue"],
32
+ svelte: [".svelte"],
33
+ python: [".py"],
34
+ ruby: [".rb"],
35
+ dart: [".dart"],
36
+ reason: [".re"],
37
+ elixir: [".ex", ".exs"],
38
+ };
39
+
40
+ export type LanguageIdExtensionMap = Record<string, `.${string}`[]> &
41
+ typeof minimumKnownExtensions;
@@ -601,8 +601,8 @@
601
601
  "graphql-object-field": {
602
602
  "match": "\\s*(([_A-Za-z][_0-9A-Za-z]*))\\s*(:)",
603
603
  "captures": {
604
- "1": { "name": "constant.object.key.graphql" },
605
- "2": { "name": "string.unquoted.graphql" },
604
+ "1": { "name": "string.unquoted.graphql" },
605
+ "2": { "name": "variable.object.key.graphql" },
606
606
  "3": { "name": "punctuation.graphql" }
607
607
  }
608
608
  },