vscode-apollo 2.0.0 → 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 (39) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/.vscode/launch.json +4 -1
  3. package/CHANGELOG.md +33 -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 +43 -10
  22. package/src/language-server/config/config.ts +26 -1
  23. package/src/language-server/config/loadConfig.ts +7 -1
  24. package/src/language-server/config/loadTsConfig.ts +70 -0
  25. package/src/language-server/config/which.d.ts +19 -0
  26. package/src/language-server/document.ts +86 -53
  27. package/src/language-server/fileSet.ts +7 -0
  28. package/src/language-server/project/base.ts +58 -316
  29. package/src/language-server/project/client.ts +730 -7
  30. package/src/language-server/project/internal.ts +349 -0
  31. package/src/language-server/project/rover/DocumentSynchronization.ts +308 -0
  32. package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
  33. package/src/language-server/project/rover/project.ts +276 -0
  34. package/src/language-server/server.ts +129 -62
  35. package/src/language-server/utilities/__tests__/source.test.ts +162 -0
  36. package/src/language-server/utilities/source.ts +38 -3
  37. package/src/language-server/workspace.ts +34 -9
  38. package/syntaxes/graphql.js.json +18 -21
  39. package/src/language-server/languageProvider.ts +0 -795
@@ -1,4 +1,4 @@
1
- import { cosmiconfig } from "cosmiconfig";
1
+ import { cosmiconfig, defaultLoaders } from "cosmiconfig";
2
2
  import { resolve } from "path";
3
3
  import { readFileSync, existsSync, lstatSync } from "fs";
4
4
  import {
@@ -9,6 +9,7 @@ import {
9
9
  import { getServiceFromKey } from "./utils";
10
10
  import { URI } from "vscode-uri";
11
11
  import { Debug } from "../utilities";
12
+ import { loadTs } from "./loadTsConfig";
12
13
 
13
14
  // config settings
14
15
  const MODULE_NAME = "apollo";
@@ -37,11 +38,16 @@ export type ConfigResult<T> = {
37
38
  } | null;
38
39
 
39
40
  // XXX load .env files automatically
41
+
40
42
  export async function loadConfig({
41
43
  configPath,
42
44
  }: LoadConfigSettings): Promise<ApolloConfig | null> {
43
45
  const explorer = cosmiconfig(MODULE_NAME, {
44
46
  searchPlaces: defaultFileNames,
47
+ loaders: {
48
+ ...defaultLoaders,
49
+ [".ts"]: loadTs,
50
+ },
45
51
  });
46
52
 
47
53
  // search can fail if a file can't be parsed (ex: a nonsense js file) so we wrap in a try/catch
@@ -0,0 +1,70 @@
1
+ import { Loader, defaultLoaders } from "cosmiconfig";
2
+ import { dirname } from "node:path";
3
+ import { rm, writeFile } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+
6
+ // implementation based on https://github.com/cosmiconfig/cosmiconfig/blob/a5a842547c13392ebb89a485b9e56d9f37e3cbd3/src/loaders.ts
7
+ // Copyright (c) 2015 David Clark licensed MIT. Full license can be found here:
8
+ // https://github.com/cosmiconfig/cosmiconfig/blob/a5a842547c13392ebb89a485b9e56d9f37e3cbd3/LICENSE
9
+
10
+ let typescript: typeof import("typescript");
11
+ export const loadTs: Loader = async function loadTs(filepath, content) {
12
+ try {
13
+ return await defaultLoaders[".ts"](filepath, content);
14
+ } catch (error) {
15
+ if (
16
+ !(error instanceof Error) ||
17
+ !error.message.includes("module is not defined")
18
+ )
19
+ throw error;
20
+ }
21
+
22
+ if (typescript === undefined) {
23
+ typescript = await import("typescript");
24
+ }
25
+ const compiledFilepath = `${filepath.slice(0, -2)}cjs`;
26
+ let transpiledContent;
27
+ try {
28
+ try {
29
+ const config = resolveTsConfig(dirname(filepath)) ?? {};
30
+ config.compilerOptions = {
31
+ ...config.compilerOptions,
32
+ module: typescript.ModuleKind.CommonJS,
33
+ moduleResolution: typescript.ModuleResolutionKind.Bundler,
34
+ target: typescript.ScriptTarget.ES2022,
35
+ noEmit: false,
36
+ };
37
+ transpiledContent = typescript.transpileModule(
38
+ content,
39
+ config,
40
+ ).outputText;
41
+ await writeFile(compiledFilepath, transpiledContent);
42
+ } catch (error: any) {
43
+ error.message = `TypeScript Error in ${filepath}:\n${error.message}`;
44
+ throw error;
45
+ }
46
+ // eslint-disable-next-line @typescript-eslint/return-await
47
+ return await defaultLoaders[".js"](compiledFilepath, transpiledContent);
48
+ } finally {
49
+ if (existsSync(compiledFilepath)) {
50
+ await rm(compiledFilepath);
51
+ }
52
+ }
53
+ };
54
+
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ function resolveTsConfig(directory: string): any {
57
+ const filePath = typescript.findConfigFile(directory, (fileName) => {
58
+ return typescript.sys.fileExists(fileName);
59
+ });
60
+ if (filePath !== undefined) {
61
+ const { config, error } = typescript.readConfigFile(filePath, (path) =>
62
+ typescript.sys.readFile(path),
63
+ );
64
+ if (error) {
65
+ throw new Error(`Error in ${filePath}: ${error.messageText.toString()}`);
66
+ }
67
+ return config;
68
+ }
69
+ return;
70
+ }
@@ -0,0 +1,19 @@
1
+ declare module "which" {
2
+ interface Options {
3
+ /** Use instead of the PATH environment variable. */
4
+ path?: string;
5
+ /** Use instead of the PATHEXT environment variable. */
6
+ pathExt?: string;
7
+ /** Return all matches, instead of just the first one. Note that this means the function returns an array of strings instead of a single string. */
8
+ all?: boolean;
9
+ }
10
+
11
+ function which(cmd: string, options?: Options): number;
12
+ namespace which {
13
+ function sync(
14
+ cmd: string,
15
+ options?: Options & { nothrow?: boolean },
16
+ ): string | null;
17
+ }
18
+ export = which;
19
+ }
@@ -2,7 +2,6 @@ import { parse, Source, DocumentNode } from "graphql";
2
2
  import { SourceLocation, getLocation } from "graphql/language/location";
3
3
 
4
4
  import {
5
- TextDocument,
6
5
  Position,
7
6
  Diagnostic,
8
7
  DiagnosticSeverity,
@@ -14,7 +13,13 @@ import {
14
13
  positionFromSourceLocation,
15
14
  rangeInContainingDocument,
16
15
  } from "./utilities/source";
16
+ import { TextDocument } from "vscode-languageserver-textdocument";
17
17
 
18
+ declare global {
19
+ interface RegExpExecArray {
20
+ indices?: Array<[number, number]>;
21
+ }
22
+ }
18
23
  export class GraphQLDocument {
19
24
  ast?: DocumentNode;
20
25
  syntaxErrors: Diagnostic[] = [];
@@ -51,74 +56,102 @@ export class GraphQLDocument {
51
56
  }
52
57
  }
53
58
 
54
- export function extractGraphQLDocuments(
59
+ export function extractGraphQLSources(
55
60
  document: TextDocument,
56
61
  tagName: string = "gql",
57
- ): GraphQLDocument[] | null {
62
+ ): Source[] | null {
58
63
  switch (document.languageId) {
59
64
  case "graphql":
60
- return [
61
- new GraphQLDocument(new Source(document.getText(), document.uri)),
62
- ];
65
+ return [new Source(document.getText(), document.uri)];
63
66
  case "javascript":
64
67
  case "javascriptreact":
65
68
  case "typescript":
66
69
  case "typescriptreact":
67
70
  case "vue":
68
71
  case "svelte":
69
- return extractGraphQLDocumentsFromJSTemplateLiterals(document, tagName);
72
+ return extractGraphQLSourcesFromJSTemplateLiterals(document, tagName);
70
73
  case "python":
71
- return extractGraphQLDocumentsFromPythonStrings(document, tagName);
74
+ return extractGraphQLSourcesFromPythonStrings(document, tagName);
72
75
  case "ruby":
73
- return extractGraphQLDocumentsFromRubyStrings(document, tagName);
76
+ return extractGraphQLSourcesFromRubyStrings(document, tagName);
74
77
  case "dart":
75
- return extractGraphQLDocumentsFromDartStrings(document, tagName);
78
+ return extractGraphQLSourcesFromDartStrings(document, tagName);
76
79
  case "reason":
77
- return extractGraphQLDocumentsFromReasonStrings(document, tagName);
80
+ return extractGraphQLSourcesFromReasonStrings(document, tagName);
78
81
  case "elixir":
79
- return extractGraphQLDocumentsFromElixirStrings(document, tagName);
82
+ return extractGraphQLSourcesFromElixirStrings(document, tagName);
80
83
  default:
81
84
  return null;
82
85
  }
83
86
  }
84
87
 
85
- function extractGraphQLDocumentsFromJSTemplateLiterals(
88
+ export function extractGraphQLDocuments(
86
89
  document: TextDocument,
87
- tagName: string,
90
+ tagName: string = "gql",
88
91
  ): GraphQLDocument[] | null {
92
+ const sources = extractGraphQLSources(document, tagName);
93
+ if (!sources) return null;
94
+ return sources.map((source) => new GraphQLDocument(source));
95
+ }
96
+
97
+ const parts = [
98
+ // normal tagged template literals
99
+ /TAG_NAME\s*(?:<.*?>\s*)?`(.*?)`/,
100
+ // template string starting with a #TAG_NAME, #graphql or #GraphQL comment
101
+ /`(\s*#[ ]*(?:TAG_NAME|graphql|GraphQL).*?)`/,
102
+ // template string preceeded by a /* TAG_NAME */, /* graphql */ or /* GraphQL */ comment
103
+ /\/\*\s*(?:TAG_NAME|graphql|GraphQL)\s*\*\/\s?`(.*?)`/,
104
+ // function call to TAG_NAME with a single template string argument
105
+ /TAG_NAME\s*(?:<.*?>\s*)?\(\s*`(.*?)`\s*\)/,
106
+ ].map((r) => r.source);
107
+
108
+ function extractGraphQLSourcesFromJSTemplateLiterals(
109
+ document: TextDocument,
110
+ tagName: string,
111
+ ): Source[] | null {
89
112
  const text = document.getText();
90
113
 
91
- const documents: GraphQLDocument[] = [];
114
+ const sources: Source[] = [];
92
115
 
93
116
  const regExp = new RegExp(
94
- `(?:${tagName}(?:\\s|\\()*\`|\`#graphql)([\\s\\S]+?)\`\\)?`,
95
- "gm",
117
+ parts.map((r) => r.replace("TAG_NAME", tagName)).join("|"),
118
+ // g: global search
119
+ // s: treat `.` as any character, including newlines
120
+ // d: save indices
121
+ "gsd",
96
122
  );
97
123
 
98
124
  let result;
99
125
  while ((result = regExp.exec(text)) !== null) {
100
- const contents = replacePlaceholdersWithWhiteSpace(result[1]);
101
- const position = document.positionAt(result.index + (tagName.length + 1));
126
+ // we have multiple alternative capture groups in the regexp, and only one of them will have a result
127
+ // so we need the index for that
128
+ const groupIndex = result.findIndex(
129
+ (part, index) => index !== 0 && part != null,
130
+ );
131
+ const contents = replacePlaceholdersWithWhiteSpace(result[groupIndex]);
132
+ const position = document.positionAt(result.indices![groupIndex][0]);
102
133
  const locationOffset: SourceLocation = {
103
134
  line: position.line + 1,
104
135
  column: position.character + 1,
105
136
  };
106
137
  const source = new Source(contents, document.uri, locationOffset);
107
- documents.push(new GraphQLDocument(source));
138
+ if (source.body.trim().length > 0) {
139
+ sources.push(source);
140
+ }
108
141
  }
109
142
 
110
- if (documents.length < 1) return null;
143
+ if (sources.length < 1) return null;
111
144
 
112
- return documents;
145
+ return sources;
113
146
  }
114
147
 
115
- function extractGraphQLDocumentsFromPythonStrings(
148
+ function extractGraphQLSourcesFromPythonStrings(
116
149
  document: TextDocument,
117
150
  tagName: string,
118
- ): GraphQLDocument[] | null {
151
+ ): Source[] | null {
119
152
  const text = document.getText();
120
153
 
121
- const documents: GraphQLDocument[] = [];
154
+ const sources: Source[] = [];
122
155
 
123
156
  const regExp = new RegExp(
124
157
  `\\b(${tagName}\\s*\\(\\s*[bfru]*("(?:"")?|'(?:'')?))([\\s\\S]+?)\\2\\s*\\)`,
@@ -134,21 +167,21 @@ function extractGraphQLDocumentsFromPythonStrings(
134
167
  column: position.character + 1,
135
168
  };
136
169
  const source = new Source(contents, document.uri, locationOffset);
137
- documents.push(new GraphQLDocument(source));
170
+ sources.push(source);
138
171
  }
139
172
 
140
- if (documents.length < 1) return null;
173
+ if (sources.length < 1) return null;
141
174
 
142
- return documents;
175
+ return sources;
143
176
  }
144
177
 
145
- function extractGraphQLDocumentsFromRubyStrings(
178
+ function extractGraphQLSourcesFromRubyStrings(
146
179
  document: TextDocument,
147
180
  tagName: string,
148
- ): GraphQLDocument[] | null {
181
+ ): Source[] | null {
149
182
  const text = document.getText();
150
183
 
151
- const documents: GraphQLDocument[] = [];
184
+ const sources: Source[] = [];
152
185
 
153
186
  const regExp = new RegExp(`(<<-${tagName})([\\s\\S]+?)${tagName}`, "gm");
154
187
 
@@ -161,21 +194,21 @@ function extractGraphQLDocumentsFromRubyStrings(
161
194
  column: position.character + 1,
162
195
  };
163
196
  const source = new Source(contents, document.uri, locationOffset);
164
- documents.push(new GraphQLDocument(source));
197
+ sources.push(source);
165
198
  }
166
199
 
167
- if (documents.length < 1) return null;
200
+ if (sources.length < 1) return null;
168
201
 
169
- return documents;
202
+ return sources;
170
203
  }
171
204
 
172
- function extractGraphQLDocumentsFromDartStrings(
205
+ function extractGraphQLSourcesFromDartStrings(
173
206
  document: TextDocument,
174
207
  tagName: string,
175
- ): GraphQLDocument[] | null {
208
+ ): Source[] | null {
176
209
  const text = document.getText();
177
210
 
178
- const documents: GraphQLDocument[] = [];
211
+ const sources: Source[] = [];
179
212
 
180
213
  const regExp = new RegExp(
181
214
  `\\b(${tagName}\\(\\s*r?("""|'''))([\\s\\S]+?)\\2\\s*\\)`,
@@ -191,26 +224,26 @@ function extractGraphQLDocumentsFromDartStrings(
191
224
  column: position.character + 1,
192
225
  };
193
226
  const source = new Source(contents, document.uri, locationOffset);
194
- documents.push(new GraphQLDocument(source));
227
+ sources.push(source);
195
228
  }
196
229
 
197
- if (documents.length < 1) return null;
230
+ if (sources.length < 1) return null;
198
231
 
199
- return documents;
232
+ return sources;
200
233
  }
201
234
 
202
- function extractGraphQLDocumentsFromReasonStrings(
235
+ function extractGraphQLSourcesFromReasonStrings(
203
236
  document: TextDocument,
204
237
  tagName: string,
205
- ): GraphQLDocument[] | null {
238
+ ): Source[] | null {
206
239
  const text = document.getText();
207
240
 
208
- const documents: GraphQLDocument[] = [];
241
+ const sources: Source[] = [];
209
242
 
210
243
  const reasonFileFilter = new RegExp(/(\[%(graphql|relay\.))/g);
211
244
 
212
245
  if (!reasonFileFilter.test(text)) {
213
- return documents;
246
+ return sources;
214
247
  }
215
248
 
216
249
  const reasonRegexp = new RegExp(
@@ -226,20 +259,20 @@ function extractGraphQLDocumentsFromReasonStrings(
226
259
  column: position.character + 1,
227
260
  };
228
261
  const source = new Source(contents, document.uri, locationOffset);
229
- documents.push(new GraphQLDocument(source));
262
+ sources.push(source);
230
263
  }
231
264
 
232
- if (documents.length < 1) return null;
265
+ if (sources.length < 1) return null;
233
266
 
234
- return documents;
267
+ return sources;
235
268
  }
236
269
 
237
- function extractGraphQLDocumentsFromElixirStrings(
270
+ function extractGraphQLSourcesFromElixirStrings(
238
271
  document: TextDocument,
239
272
  tagName: string,
240
- ): GraphQLDocument[] | null {
273
+ ): Source[] | null {
241
274
  const text = document.getText();
242
- const documents: GraphQLDocument[] = [];
275
+ const sources: Source[] = [];
243
276
 
244
277
  const regExp = new RegExp(
245
278
  `\\b(${tagName}\\(\\s*r?("""))([\\s\\S]+?)\\2\\s*\\)`,
@@ -255,12 +288,12 @@ function extractGraphQLDocumentsFromElixirStrings(
255
288
  column: position.character + 1,
256
289
  };
257
290
  const source = new Source(contents, document.uri, locationOffset);
258
- documents.push(new GraphQLDocument(source));
291
+ sources.push(source);
259
292
  }
260
293
 
261
- if (documents.length < 1) return null;
294
+ if (sources.length < 1) return null;
262
295
 
263
- return documents;
296
+ return sources;
264
297
  }
265
298
 
266
299
  function replacePlaceholdersWithWhiteSpace(content: string) {
@@ -30,6 +30,13 @@ export class FileSet {
30
30
  this.excludes = excludes;
31
31
  }
32
32
 
33
+ pushIncludes(files: string[]) {
34
+ this.includes.push(...files);
35
+ }
36
+ pushExcludes(files: string[]) {
37
+ this.excludes.push(...files);
38
+ }
39
+
33
40
  includesFile(filePath: string): boolean {
34
41
  const normalizedFilePath = normalizeURI(filePath);
35
42