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.
- package/.circleci/config.yml +1 -1
- package/.vscode/launch.json +4 -1
- package/CHANGELOG.md +33 -0
- package/package.json +9 -3
- package/renovate.json +2 -1
- package/sampleWorkspace/localSchema/src/test.js +3 -0
- package/sampleWorkspace/rover/apollo.config.js +3 -0
- package/sampleWorkspace/rover/src/test.graphql +14 -0
- package/sampleWorkspace/rover/src/test.js +30 -0
- package/sampleWorkspace/sampleWorkspace.code-workspace +25 -19
- package/src/language-server/__tests__/document.test.ts +161 -3
- package/src/language-server/__tests__/fixtures/TypeScript.tmLanguage.json +5749 -0
- package/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts +41 -0
- package/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts.snap +185 -0
- package/src/language-server/__tests__/fixtures/documents/functionCall.ts +93 -0
- package/src/language-server/__tests__/fixtures/documents/functionCall.ts.snap +431 -0
- package/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts +80 -0
- package/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts.snap +353 -0
- package/src/language-server/__tests__/fixtures/documents/templateWithComment.ts +38 -0
- package/src/language-server/__tests__/fixtures/documents/templateWithComment.ts.snap +123 -0
- package/src/language-server/config/__tests__/loadConfig.ts +43 -10
- package/src/language-server/config/config.ts +26 -1
- package/src/language-server/config/loadConfig.ts +7 -1
- package/src/language-server/config/loadTsConfig.ts +70 -0
- package/src/language-server/config/which.d.ts +19 -0
- package/src/language-server/document.ts +86 -53
- package/src/language-server/fileSet.ts +7 -0
- package/src/language-server/project/base.ts +58 -316
- package/src/language-server/project/client.ts +730 -7
- package/src/language-server/project/internal.ts +349 -0
- package/src/language-server/project/rover/DocumentSynchronization.ts +308 -0
- package/src/language-server/project/rover/__tests__/DocumentSynchronization.test.ts +302 -0
- package/src/language-server/project/rover/project.ts +276 -0
- package/src/language-server/server.ts +129 -62
- package/src/language-server/utilities/__tests__/source.test.ts +162 -0
- package/src/language-server/utilities/source.ts +38 -3
- package/src/language-server/workspace.ts +34 -9
- package/syntaxes/graphql.js.json +18 -21
- 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
|
|
59
|
+
export function extractGraphQLSources(
|
|
55
60
|
document: TextDocument,
|
|
56
61
|
tagName: string = "gql",
|
|
57
|
-
):
|
|
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
|
|
72
|
+
return extractGraphQLSourcesFromJSTemplateLiterals(document, tagName);
|
|
70
73
|
case "python":
|
|
71
|
-
return
|
|
74
|
+
return extractGraphQLSourcesFromPythonStrings(document, tagName);
|
|
72
75
|
case "ruby":
|
|
73
|
-
return
|
|
76
|
+
return extractGraphQLSourcesFromRubyStrings(document, tagName);
|
|
74
77
|
case "dart":
|
|
75
|
-
return
|
|
78
|
+
return extractGraphQLSourcesFromDartStrings(document, tagName);
|
|
76
79
|
case "reason":
|
|
77
|
-
return
|
|
80
|
+
return extractGraphQLSourcesFromReasonStrings(document, tagName);
|
|
78
81
|
case "elixir":
|
|
79
|
-
return
|
|
82
|
+
return extractGraphQLSourcesFromElixirStrings(document, tagName);
|
|
80
83
|
default:
|
|
81
84
|
return null;
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function
|
|
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
|
|
114
|
+
const sources: Source[] = [];
|
|
92
115
|
|
|
93
116
|
const regExp = new RegExp(
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
138
|
+
if (source.body.trim().length > 0) {
|
|
139
|
+
sources.push(source);
|
|
140
|
+
}
|
|
108
141
|
}
|
|
109
142
|
|
|
110
|
-
if (
|
|
143
|
+
if (sources.length < 1) return null;
|
|
111
144
|
|
|
112
|
-
return
|
|
145
|
+
return sources;
|
|
113
146
|
}
|
|
114
147
|
|
|
115
|
-
function
|
|
148
|
+
function extractGraphQLSourcesFromPythonStrings(
|
|
116
149
|
document: TextDocument,
|
|
117
150
|
tagName: string,
|
|
118
|
-
):
|
|
151
|
+
): Source[] | null {
|
|
119
152
|
const text = document.getText();
|
|
120
153
|
|
|
121
|
-
const
|
|
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
|
-
|
|
170
|
+
sources.push(source);
|
|
138
171
|
}
|
|
139
172
|
|
|
140
|
-
if (
|
|
173
|
+
if (sources.length < 1) return null;
|
|
141
174
|
|
|
142
|
-
return
|
|
175
|
+
return sources;
|
|
143
176
|
}
|
|
144
177
|
|
|
145
|
-
function
|
|
178
|
+
function extractGraphQLSourcesFromRubyStrings(
|
|
146
179
|
document: TextDocument,
|
|
147
180
|
tagName: string,
|
|
148
|
-
):
|
|
181
|
+
): Source[] | null {
|
|
149
182
|
const text = document.getText();
|
|
150
183
|
|
|
151
|
-
const
|
|
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
|
-
|
|
197
|
+
sources.push(source);
|
|
165
198
|
}
|
|
166
199
|
|
|
167
|
-
if (
|
|
200
|
+
if (sources.length < 1) return null;
|
|
168
201
|
|
|
169
|
-
return
|
|
202
|
+
return sources;
|
|
170
203
|
}
|
|
171
204
|
|
|
172
|
-
function
|
|
205
|
+
function extractGraphQLSourcesFromDartStrings(
|
|
173
206
|
document: TextDocument,
|
|
174
207
|
tagName: string,
|
|
175
|
-
):
|
|
208
|
+
): Source[] | null {
|
|
176
209
|
const text = document.getText();
|
|
177
210
|
|
|
178
|
-
const
|
|
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
|
-
|
|
227
|
+
sources.push(source);
|
|
195
228
|
}
|
|
196
229
|
|
|
197
|
-
if (
|
|
230
|
+
if (sources.length < 1) return null;
|
|
198
231
|
|
|
199
|
-
return
|
|
232
|
+
return sources;
|
|
200
233
|
}
|
|
201
234
|
|
|
202
|
-
function
|
|
235
|
+
function extractGraphQLSourcesFromReasonStrings(
|
|
203
236
|
document: TextDocument,
|
|
204
237
|
tagName: string,
|
|
205
|
-
):
|
|
238
|
+
): Source[] | null {
|
|
206
239
|
const text = document.getText();
|
|
207
240
|
|
|
208
|
-
const
|
|
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
|
|
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
|
-
|
|
262
|
+
sources.push(source);
|
|
230
263
|
}
|
|
231
264
|
|
|
232
|
-
if (
|
|
265
|
+
if (sources.length < 1) return null;
|
|
233
266
|
|
|
234
|
-
return
|
|
267
|
+
return sources;
|
|
235
268
|
}
|
|
236
269
|
|
|
237
|
-
function
|
|
270
|
+
function extractGraphQLSourcesFromElixirStrings(
|
|
238
271
|
document: TextDocument,
|
|
239
272
|
tagName: string,
|
|
240
|
-
):
|
|
273
|
+
): Source[] | null {
|
|
241
274
|
const text = document.getText();
|
|
242
|
-
const
|
|
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
|
-
|
|
291
|
+
sources.push(source);
|
|
259
292
|
}
|
|
260
293
|
|
|
261
|
-
if (
|
|
294
|
+
if (sources.length < 1) return null;
|
|
262
295
|
|
|
263
|
-
return
|
|
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
|
|