vscode-apollo 2.2.1 → 2.3.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/.circleci/config.yml +1 -17
- package/.github/workflows/E2E.yml +27 -0
- package/.github/workflows/build-prs.yml +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +71 -52
- package/jest.e2e.config.js +2 -0
- package/package.json +31 -3
- package/renovate.json +5 -5
- package/sampleWorkspace/localSchemaArray/apollo.config.json +9 -0
- package/sampleWorkspace/localSchemaArray/src/test.js +1 -1
- package/sampleWorkspace/rover/apollo.config.yaml +3 -0
- package/sampleWorkspace/rover/src/test.graphql +11 -10
- package/sampleWorkspace/rover/supergraph.yaml +0 -0
- package/schemas/apollo.config.schema.json +184 -0
- package/src/__e2e__/runTests.js +1 -0
- package/src/build.js +53 -1
- package/src/language-server/__e2e__/clientSchema.e2e.ts +58 -28
- package/src/language-server/__e2e__/httpSchema.e2e.ts +23 -4
- package/src/language-server/__e2e__/localSchema.e2e.ts +23 -4
- package/src/language-server/__e2e__/localSchemaArray.e2e.ts +31 -6
- package/src/language-server/__e2e__/rover.e2e.ts +150 -0
- package/src/language-server/__e2e__/studioGraph.e2e.ts +18 -6
- package/src/language-server/__e2e__/utils.ts +114 -13
- package/src/language-server/config/__tests__/loadConfig.ts +35 -2
- package/src/language-server/config/cache-busting-resolver.js +65 -0
- package/src/language-server/config/cache-busting-resolver.types.ts +45 -0
- package/src/language-server/config/config.ts +133 -57
- package/src/language-server/config/loadConfig.ts +27 -6
- package/src/language-server/config/loadTsConfig.ts +74 -38
- package/src/language-server/project/base.ts +8 -8
- package/src/language-server/project/rover/project.ts +6 -0
- package/src/language-server/server.ts +8 -7
- package/src/language-server/workspace.ts +2 -5
- package/src/languageServerClient.ts +3 -1
- package/sampleWorkspace/localSchemaArray/apollo.config.js +0 -12
- package/sampleWorkspace/rover/apollo.config.js +0 -3
- /package/sampleWorkspace/localSchema/{apollo.config.js → apollo.config.ts} +0 -0
|
@@ -8,6 +8,25 @@ function resolve(file: string) {
|
|
|
8
8
|
return join(__dirname, "..", "..", "..", "sampleWorkspace", file);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export type GetPositionFn = ReturnType<typeof getPositionForEditor>;
|
|
12
|
+
export function getPositionForEditor(editor: vscode.TextEditor) {
|
|
13
|
+
return function getPosition(cursor: `${string}|${string}`) {
|
|
14
|
+
if (cursor.indexOf("|") !== cursor.lastIndexOf("|")) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"`getPosition` cursor description can only contain one |",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const text = editor.document.getText();
|
|
20
|
+
const idx = text.indexOf(cursor.replace("|", ""));
|
|
21
|
+
if (idx !== text.lastIndexOf(cursor.replace("|", ""))) {
|
|
22
|
+
throw new Error("`getPosition` cursor description is not unique");
|
|
23
|
+
}
|
|
24
|
+
const cursorIndex = idx + cursor.indexOf("|");
|
|
25
|
+
const position = editor.document.positionAt(cursorIndex);
|
|
26
|
+
return position;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
export async function closeAllEditors() {
|
|
12
31
|
while (vscode.window.visibleTextEditors.length > 0) {
|
|
13
32
|
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
|
|
@@ -50,23 +69,28 @@ export function waitForLSP(file: string) {
|
|
|
50
69
|
});
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
export async function
|
|
72
|
+
export async function getCompletionItems(
|
|
54
73
|
editor: vscode.TextEditor,
|
|
55
|
-
|
|
56
|
-
expected: Array<[label: string, detail: string]>,
|
|
74
|
+
position: vscode.Position,
|
|
57
75
|
) {
|
|
76
|
+
let result: { label: string; detail: string | undefined }[] | undefined = [];
|
|
58
77
|
await waitFor(async () => {
|
|
59
|
-
editor.selection = new vscode.Selection(
|
|
78
|
+
editor.selection = new vscode.Selection(
|
|
79
|
+
position.line,
|
|
80
|
+
position.character,
|
|
81
|
+
position.line,
|
|
82
|
+
position.character,
|
|
83
|
+
);
|
|
60
84
|
// without this, the completion list is not updated
|
|
61
85
|
await scheduler.wait(300);
|
|
62
86
|
const completions =
|
|
63
87
|
await vscode.commands.executeCommand<vscode.CompletionList>(
|
|
64
88
|
"vscode.executeCompletionItemProvider",
|
|
65
89
|
editor.document.uri,
|
|
66
|
-
|
|
90
|
+
position,
|
|
67
91
|
);
|
|
68
|
-
|
|
69
|
-
const labels = completions.items.
|
|
92
|
+
expect(completions.items).not.toHaveLength(0);
|
|
93
|
+
const labels = completions.items.map((item) =>
|
|
70
94
|
typeof item.label === "string"
|
|
71
95
|
? { label: item.label, detail: "" }
|
|
72
96
|
: {
|
|
@@ -74,23 +98,27 @@ export async function testCompletion(
|
|
|
74
98
|
detail: item.detail,
|
|
75
99
|
},
|
|
76
100
|
);
|
|
77
|
-
|
|
78
|
-
expected.map(([label, detail]) => ({ label, detail })),
|
|
79
|
-
);
|
|
101
|
+
result = labels;
|
|
80
102
|
});
|
|
103
|
+
return result;
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
export async function getHover(
|
|
84
107
|
editor: vscode.TextEditor,
|
|
85
|
-
|
|
108
|
+
position: vscode.Position,
|
|
86
109
|
) {
|
|
87
|
-
editor.selection = new vscode.Selection(
|
|
110
|
+
editor.selection = new vscode.Selection(
|
|
111
|
+
position.line,
|
|
112
|
+
position.character,
|
|
113
|
+
position.line,
|
|
114
|
+
position.character,
|
|
115
|
+
);
|
|
88
116
|
// without this, the completion list is not updated
|
|
89
117
|
await scheduler.wait(300);
|
|
90
118
|
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
|
|
91
119
|
"vscode.executeHoverProvider",
|
|
92
120
|
editor.document.uri,
|
|
93
|
-
|
|
121
|
+
position,
|
|
94
122
|
);
|
|
95
123
|
|
|
96
124
|
const item = hovers[0];
|
|
@@ -149,3 +177,76 @@ export async function reloadService() {
|
|
|
149
177
|
await reloaded;
|
|
150
178
|
await scheduler.wait(100);
|
|
151
179
|
}
|
|
180
|
+
|
|
181
|
+
export async function getFullSemanticTokens(editor: vscode.TextEditor) {
|
|
182
|
+
const legend = await vscode.commands.executeCommand<
|
|
183
|
+
vscode.SemanticTokensLegend | undefined
|
|
184
|
+
>(
|
|
185
|
+
// https://github.com/microsoft/vscode/blob/d90ab31527203cdb15056df0dc84ab9ddcbbde40/src/vs/workbench/api/common/extHostApiCommands.ts#L220
|
|
186
|
+
"vscode.provideDocumentSemanticTokensLegend",
|
|
187
|
+
editor.document.uri,
|
|
188
|
+
);
|
|
189
|
+
expect(legend).toBeDefined();
|
|
190
|
+
const tokens = await vscode.commands.executeCommand<
|
|
191
|
+
vscode.SemanticTokens | undefined
|
|
192
|
+
>(
|
|
193
|
+
// https://github.com/microsoft/vscode/blob/d90ab31527203cdb15056df0dc84ab9ddcbbde40/src/vs/workbench/api/common/extHostApiCommands.ts#L229
|
|
194
|
+
"vscode.provideDocumentSemanticTokens",
|
|
195
|
+
editor.document.uri,
|
|
196
|
+
);
|
|
197
|
+
expect(tokens).toBeDefined();
|
|
198
|
+
|
|
199
|
+
return decodeSemanticTokens(tokens!, legend!);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function decodeSemanticTokens(
|
|
203
|
+
tokens: vscode.SemanticTokens,
|
|
204
|
+
legend: vscode.SemanticTokensLegend,
|
|
205
|
+
) {
|
|
206
|
+
const tokenArr = Array.from(tokens.data);
|
|
207
|
+
const decodedTokens: {
|
|
208
|
+
startPosition: vscode.Position;
|
|
209
|
+
endPosition: vscode.Position;
|
|
210
|
+
tokenType: string;
|
|
211
|
+
tokenModifiers: string[];
|
|
212
|
+
}[] = [];
|
|
213
|
+
let line = 0,
|
|
214
|
+
start = 0;
|
|
215
|
+
for (let pos = 0; pos < tokenArr.length; pos += 5) {
|
|
216
|
+
const [deltaLine, deltaStart, length, tokenType, tokenModifiers] =
|
|
217
|
+
tokenArr.slice(pos, pos + 5);
|
|
218
|
+
if (deltaLine) {
|
|
219
|
+
line += deltaLine;
|
|
220
|
+
start = 0;
|
|
221
|
+
}
|
|
222
|
+
start += deltaStart;
|
|
223
|
+
const decodedModifiers: string[] = [];
|
|
224
|
+
for (let modifiers = tokenModifiers; modifiers > 0; modifiers >>= 1) {
|
|
225
|
+
decodedModifiers.push(legend.tokenModifiers[modifiers & 0xf]);
|
|
226
|
+
}
|
|
227
|
+
const startPosition = new vscode.Position(line, start);
|
|
228
|
+
const endPosition = startPosition.translate(0, length);
|
|
229
|
+
decodedTokens.push({
|
|
230
|
+
startPosition,
|
|
231
|
+
endPosition,
|
|
232
|
+
tokenType: legend.tokenTypes[tokenType],
|
|
233
|
+
tokenModifiers: decodedModifiers,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return decodedTokens;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function getDefinitions(
|
|
240
|
+
editor: vscode.TextEditor,
|
|
241
|
+
position: vscode.Position,
|
|
242
|
+
) {
|
|
243
|
+
return vscode.commands.executeCommand<
|
|
244
|
+
// this is not the correct type, but the best match with public types I could find
|
|
245
|
+
vscode.LocationLink[]
|
|
246
|
+
>(
|
|
247
|
+
// https://github.com/microsoft/vscode/blob/d90ab31527203cdb15056df0dc84ab9ddcbbde40/src/vs/workbench/api/common/extHostApiCommands.ts#L87
|
|
248
|
+
"vscode.executeDefinitionProvider",
|
|
249
|
+
editor.document.uri,
|
|
250
|
+
position,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -174,7 +174,10 @@ Object {
|
|
|
174
174
|
expect(config?.client?.service).toEqual("hello");
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
// we skip these tests because ts-jest transpiles every `import` down to a `require` call,
|
|
178
|
+
// which messes up all the importing anyways.
|
|
179
|
+
// we have to rely on our E2E tests to ensure that config files resolve correctly
|
|
180
|
+
it.skip("loads config from a cjs file", async () => {
|
|
178
181
|
writeFilesToDir(dir, {
|
|
179
182
|
"apollo.config.cjs": `module.exports = {"client": {"service": "hello"} }`,
|
|
180
183
|
});
|
|
@@ -182,13 +185,43 @@ Object {
|
|
|
182
185
|
expect(config?.client?.service).toEqual("hello");
|
|
183
186
|
});
|
|
184
187
|
|
|
185
|
-
it("loads config from a mjs file", async () => {
|
|
188
|
+
it.skip("loads config from a mjs file", async () => {
|
|
186
189
|
writeFilesToDir(dir, {
|
|
187
190
|
"apollo.config.mjs": `export default {"client": {"service": "hello"} }`,
|
|
188
191
|
});
|
|
189
192
|
const config = await loadConfig({ configPath: dirPath });
|
|
190
193
|
expect(config?.client?.service).toEqual("hello");
|
|
191
194
|
});
|
|
195
|
+
|
|
196
|
+
it("loads config from a yml file", async () => {
|
|
197
|
+
writeFilesToDir(dir, {
|
|
198
|
+
"apollo.config.yml": `
|
|
199
|
+
client:
|
|
200
|
+
service: hello
|
|
201
|
+
`,
|
|
202
|
+
});
|
|
203
|
+
const config = await loadConfig({ configPath: dirPath });
|
|
204
|
+
expect(config?.client?.service).toEqual("hello");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("loads config from a yaml file", async () => {
|
|
208
|
+
writeFilesToDir(dir, {
|
|
209
|
+
"apollo.config.yaml": `
|
|
210
|
+
client:
|
|
211
|
+
service: hello
|
|
212
|
+
`,
|
|
213
|
+
});
|
|
214
|
+
const config = await loadConfig({ configPath: dirPath });
|
|
215
|
+
expect(config?.client?.service).toEqual("hello");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("loads config from a json file", async () => {
|
|
219
|
+
writeFilesToDir(dir, {
|
|
220
|
+
"apollo.config.json": `{"client": /* testing jsonc */ {"service": "hello"} }`,
|
|
221
|
+
});
|
|
222
|
+
const config = await loadConfig({ configPath: dirPath });
|
|
223
|
+
expect(config?.client?.service).toEqual("hello");
|
|
224
|
+
});
|
|
192
225
|
});
|
|
193
226
|
|
|
194
227
|
describe("errors", () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const { pathToFileURL } = require("node:url");
|
|
3
|
+
|
|
4
|
+
/** @import { ResolveContext, ResolutionResult, LoadResult, ImportContext } from "./cache-busting-resolver.types" */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} specifier
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function bustFileName(specifier) {
|
|
11
|
+
const url = pathToFileURL(specifier);
|
|
12
|
+
url.pathname = url.pathname + "." + Date.now() + ".js";
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @param {string} specifier
|
|
19
|
+
* @param {ResolveContext} context
|
|
20
|
+
* @param {(specifier: string,context: ResolveContext) => Promise<ResolutionResult>} nextResolve
|
|
21
|
+
* @returns {Promise<ResolutionResult>}
|
|
22
|
+
*/
|
|
23
|
+
async function resolve(specifier, context, nextResolve) {
|
|
24
|
+
if (context.importAttributes.as !== "cachebust") {
|
|
25
|
+
return nextResolve(specifier, context);
|
|
26
|
+
}
|
|
27
|
+
if (context.importAttributes.format) {
|
|
28
|
+
// no need to resolve at all, we have all necessary information
|
|
29
|
+
return {
|
|
30
|
+
url: bustFileName(specifier),
|
|
31
|
+
format: context.importAttributes.format,
|
|
32
|
+
importAttributes: context.importAttributes,
|
|
33
|
+
shortCircuit: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const result = await nextResolve(specifier, context);
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
url: bustFileName(result.url),
|
|
40
|
+
importAttributes: context.importAttributes,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param {string} url
|
|
47
|
+
* @param {ImportContext} context
|
|
48
|
+
* @param {(url: string, context: ImportContext) => Promise<LoadResult>} nextLoad
|
|
49
|
+
* @returns {Promise<LoadResult>}
|
|
50
|
+
*/
|
|
51
|
+
async function load(url, context, nextLoad) {
|
|
52
|
+
if (context.importAttributes.as !== "cachebust") {
|
|
53
|
+
return nextLoad(url, context);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
format: context.format || "module",
|
|
57
|
+
shortCircuit: true,
|
|
58
|
+
source: context.importAttributes.contents,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
resolve,
|
|
64
|
+
load,
|
|
65
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
export type ImportAttributes =
|
|
4
|
+
| {
|
|
5
|
+
as: "cachebust";
|
|
6
|
+
contents: string;
|
|
7
|
+
format?: Format;
|
|
8
|
+
}
|
|
9
|
+
| { as?: undefined };
|
|
10
|
+
|
|
11
|
+
type Format =
|
|
12
|
+
| "builtin"
|
|
13
|
+
| "commonjs"
|
|
14
|
+
| "json"
|
|
15
|
+
| "module"
|
|
16
|
+
| "wasm"
|
|
17
|
+
| null
|
|
18
|
+
| undefined;
|
|
19
|
+
|
|
20
|
+
export interface ResolveContext {
|
|
21
|
+
conditions: string[];
|
|
22
|
+
importAttributes: ImportAttributes;
|
|
23
|
+
parentURL?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImportContext {
|
|
27
|
+
conditions: string[];
|
|
28
|
+
importAttributes: ImportAttributes;
|
|
29
|
+
format: Format;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ResolutionResult {
|
|
33
|
+
format: Format;
|
|
34
|
+
importAttributes?: ImportAttributes;
|
|
35
|
+
shortCircuit?: boolean;
|
|
36
|
+
url: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LoadResult {
|
|
40
|
+
format: Format;
|
|
41
|
+
shortCircuit?: boolean;
|
|
42
|
+
source: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {};
|
|
@@ -25,7 +25,10 @@ function ignoredFieldWarning(
|
|
|
25
25
|
Debug.warning(getMessage(ctx.path.join(".")));
|
|
26
26
|
}
|
|
27
27
|
})
|
|
28
|
-
.optional()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
`This option is no longer supported, please remove it from your configuration file.`,
|
|
31
|
+
);
|
|
29
32
|
}
|
|
30
33
|
export interface Context {
|
|
31
34
|
apiKey?: string;
|
|
@@ -34,53 +37,113 @@ export interface Context {
|
|
|
34
37
|
}
|
|
35
38
|
const contextStore = new AsyncLocalStorage<Context>();
|
|
36
39
|
|
|
37
|
-
const
|
|
40
|
+
const NAME_DESCRIPTION =
|
|
41
|
+
"The name your project will be referred to by the Apollo GraphQL extension.";
|
|
38
42
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const studioServiceConfig = z
|
|
44
|
+
.string()
|
|
45
|
+
.describe(
|
|
46
|
+
"The name of the Apollo Studio graph to use. Alternatively pass in an object to configure a local schema.",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const remoteServiceConfig = z
|
|
50
|
+
.object({
|
|
51
|
+
name: z.string().optional().describe(NAME_DESCRIPTION),
|
|
52
|
+
url: z
|
|
53
|
+
.string()
|
|
54
|
+
.describe(
|
|
55
|
+
"URL of a GraphQL to use for the GraphQL Schema for this project. Needs introspection enabled.",
|
|
56
|
+
),
|
|
57
|
+
headers: z
|
|
58
|
+
.record(z.string())
|
|
59
|
+
.default({})
|
|
60
|
+
.describe("Additional headers to send to the server."),
|
|
61
|
+
skipSSLValidation: z
|
|
62
|
+
.boolean()
|
|
63
|
+
.default(false)
|
|
64
|
+
.describe(
|
|
65
|
+
"Skip SSL validation. May be required for self-signed certificates.",
|
|
66
|
+
),
|
|
67
|
+
})
|
|
68
|
+
.describe("Configuration for using a local schema from a URL.");
|
|
45
69
|
export type RemoteServiceConfig = z.infer<typeof remoteServiceConfig>;
|
|
46
70
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
const LOCAL_SCHEMA_FILE_DESCRIPTION =
|
|
72
|
+
"Path to a local schema file to use as GraphQL Schema for this project. Can be a string or an array of strings to merge multiple partial schemas into one.";
|
|
73
|
+
const localServiceConfig = z
|
|
74
|
+
.object({
|
|
75
|
+
name: z.string().optional().describe(NAME_DESCRIPTION),
|
|
76
|
+
localSchemaFile: z
|
|
77
|
+
.union([
|
|
78
|
+
z.string().describe(LOCAL_SCHEMA_FILE_DESCRIPTION),
|
|
79
|
+
z.array(z.string()).describe(LOCAL_SCHEMA_FILE_DESCRIPTION),
|
|
80
|
+
])
|
|
81
|
+
.describe(LOCAL_SCHEMA_FILE_DESCRIPTION),
|
|
82
|
+
})
|
|
83
|
+
.describe("Configuration for using a local schema from a file.");
|
|
51
84
|
export type LocalServiceConfig = z.infer<typeof localServiceConfig>;
|
|
52
85
|
|
|
53
|
-
const clientServiceConfig = z
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
86
|
+
const clientServiceConfig = z
|
|
87
|
+
.preprocess(
|
|
88
|
+
(value) => value || contextStore.getStore()?.serviceName,
|
|
89
|
+
z.union([studioServiceConfig, remoteServiceConfig, localServiceConfig]),
|
|
90
|
+
)
|
|
91
|
+
.describe(
|
|
92
|
+
"A string to refer to a graph in Apollo Studio, or an object for a local schema.",
|
|
93
|
+
);
|
|
57
94
|
export type ClientServiceConfig = z.infer<typeof clientServiceConfig>;
|
|
58
95
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
const VALIDATION_RULES_DESCRIPTION =
|
|
97
|
+
"Additional validation rules to check for. To use this feature, please use a configuration file format that allows passing JavaScript objects.";
|
|
98
|
+
export const clientConfig = z
|
|
99
|
+
.object({
|
|
100
|
+
service: clientServiceConfig,
|
|
101
|
+
validationRules: z
|
|
102
|
+
.union([
|
|
103
|
+
z
|
|
104
|
+
.array(z.custom<ValidationRule>())
|
|
105
|
+
.describe(VALIDATION_RULES_DESCRIPTION),
|
|
106
|
+
z
|
|
107
|
+
.function()
|
|
108
|
+
.args(z.custom<ValidationRule>())
|
|
109
|
+
.returns(z.boolean())
|
|
110
|
+
.describe(VALIDATION_RULES_DESCRIPTION),
|
|
111
|
+
])
|
|
112
|
+
.optional()
|
|
113
|
+
.describe(VALIDATION_RULES_DESCRIPTION),
|
|
114
|
+
// maybe shared with rover?
|
|
115
|
+
includes: z
|
|
116
|
+
.array(z.string())
|
|
117
|
+
.optional()
|
|
118
|
+
.describe(
|
|
119
|
+
"An array of glob patterns this project should be active on. The Apollo GraphQL extension will only support IntelliSense-like features in files listed here.",
|
|
120
|
+
),
|
|
121
|
+
// maybe shared with rover?
|
|
122
|
+
excludes: z
|
|
123
|
+
.array(z.string())
|
|
124
|
+
.default(["**/node_modules", "**/__tests__"])
|
|
125
|
+
.describe(
|
|
126
|
+
"Files to exclude from this project. The Apollo GraphQL extension will not provide IntelliSense-like features in these files.",
|
|
127
|
+
),
|
|
128
|
+
// maybe shared with rover?
|
|
129
|
+
tagName: z
|
|
130
|
+
.string()
|
|
131
|
+
.default("gql")
|
|
132
|
+
.describe(
|
|
133
|
+
"The name of the template literal tag or function used in JavaScript files to declare GraphQL Documents.",
|
|
134
|
+
),
|
|
135
|
+
// removed:
|
|
136
|
+
clientOnlyDirectives: ignoredFieldWarning(),
|
|
137
|
+
clientSchemaDirectives: ignoredFieldWarning(),
|
|
138
|
+
statsWindow: ignoredFieldWarning(),
|
|
139
|
+
name: ignoredFieldWarning(),
|
|
140
|
+
referenceId: ignoredFieldWarning(),
|
|
141
|
+
version: ignoredFieldWarning(),
|
|
142
|
+
})
|
|
143
|
+
.describe("Configuration for a Client project.");
|
|
81
144
|
export type ClientConfigFormat = z.infer<typeof clientConfig>;
|
|
82
145
|
|
|
83
|
-
const roverConfig = z.object({
|
|
146
|
+
export const roverConfig = z.object({
|
|
84
147
|
bin: z
|
|
85
148
|
.preprocess(
|
|
86
149
|
(val) => val || which.sync("rover", { nothrow: true }) || undefined,
|
|
@@ -104,8 +167,9 @@ const roverConfig = z.object({
|
|
|
104
167
|
message:
|
|
105
168
|
"Rover binary is not marked as an executable. If you are using OS X or Linux, ensure to set the executable bit.",
|
|
106
169
|
},
|
|
107
|
-
)
|
|
108
|
-
|
|
170
|
+
)
|
|
171
|
+
.describe("The path to your Rover binary. If omitted, will look in PATH."),
|
|
172
|
+
profile: z.string().optional().describe("The name of the profile to use."),
|
|
109
173
|
supergraphConfig: z
|
|
110
174
|
.preprocess((value) => {
|
|
111
175
|
if (value !== undefined) return value;
|
|
@@ -124,24 +188,36 @@ const roverConfig = z.object({
|
|
|
124
188
|
});
|
|
125
189
|
type RoverConfigFormat = z.infer<typeof roverConfig>;
|
|
126
190
|
|
|
127
|
-
const engineConfig = z
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
z
|
|
137
|
-
|
|
138
|
-
|
|
191
|
+
export const engineConfig = z
|
|
192
|
+
.object({
|
|
193
|
+
endpoint: z
|
|
194
|
+
.string()
|
|
195
|
+
.default(
|
|
196
|
+
process.env.APOLLO_ENGINE_ENDPOINT ||
|
|
197
|
+
"https://graphql.api.apollographql.com/api/graphql",
|
|
198
|
+
)
|
|
199
|
+
.describe("The URL of the Apollo Studio API."),
|
|
200
|
+
apiKey: z
|
|
201
|
+
.preprocess(
|
|
202
|
+
(val) => val || contextStore.getStore()?.apiKey,
|
|
203
|
+
z.string().optional(),
|
|
204
|
+
)
|
|
205
|
+
.describe(
|
|
206
|
+
"The API key to use for Apollo Studio. If possible, use a `.env` file or `.env.local` file instead to store secrets like this.",
|
|
207
|
+
),
|
|
208
|
+
})
|
|
209
|
+
.describe("Network configuration for Apollo Studio API.");
|
|
139
210
|
export type EngineConfig = z.infer<typeof engineConfig>;
|
|
140
211
|
|
|
141
|
-
const baseConfig = z.object({
|
|
212
|
+
export const baseConfig = z.object({
|
|
142
213
|
engine: engineConfig.default({}),
|
|
143
|
-
client: z.unknown().optional(),
|
|
144
|
-
|
|
214
|
+
client: z.unknown().optional().describe(clientConfig.description!),
|
|
215
|
+
...ifRoverAvailable(
|
|
216
|
+
{
|
|
217
|
+
rover: z.unknown().optional(),
|
|
218
|
+
},
|
|
219
|
+
{},
|
|
220
|
+
),
|
|
145
221
|
service: ignoredFieldWarning(
|
|
146
222
|
(path) =>
|
|
147
223
|
`Service-type projects are no longer supported. Please remove the "${path}" field from your configuration file.`,
|
|
@@ -268,7 +344,7 @@ export abstract class ApolloConfig {
|
|
|
268
344
|
get configDirURI() {
|
|
269
345
|
// if the filepath has a _file_ in it, then we get its dir
|
|
270
346
|
return this.configURI &&
|
|
271
|
-
this.configURI.fsPath.match(/\.(ts|js|cjs|mjs|json)$/i)
|
|
347
|
+
this.configURI.fsPath.match(/\.(ts|js|cjs|mjs|yaml|yml|json)$/i)
|
|
272
348
|
? URI.parse(dirname(this.configURI.fsPath))
|
|
273
349
|
: this.configURI;
|
|
274
350
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cosmiconfig, defaultLoaders } from "cosmiconfig";
|
|
1
|
+
import { cosmiconfig, defaultLoaders, Loader } from "cosmiconfig";
|
|
2
2
|
import { dirname, resolve } from "path";
|
|
3
3
|
import { readFileSync, existsSync, lstatSync } from "fs";
|
|
4
4
|
import {
|
|
@@ -9,18 +9,22 @@ import {
|
|
|
9
9
|
import { getServiceFromKey } from "./utils";
|
|
10
10
|
import { URI } from "vscode-uri";
|
|
11
11
|
import { Debug } from "../utilities";
|
|
12
|
-
import {
|
|
12
|
+
import { ParseError, parse as parseJsonC } from "jsonc-parser";
|
|
13
|
+
import { loadJs, loadTs } from "./loadTsConfig";
|
|
13
14
|
|
|
14
15
|
// config settings
|
|
15
16
|
const MODULE_NAME = "apollo";
|
|
16
|
-
const
|
|
17
|
+
export const supportedConfigFileNames = [
|
|
17
18
|
"package.json",
|
|
18
19
|
`${MODULE_NAME}.config.js`,
|
|
19
20
|
`${MODULE_NAME}.config.ts`,
|
|
20
21
|
`${MODULE_NAME}.config.mjs`,
|
|
21
22
|
`${MODULE_NAME}.config.cjs`,
|
|
23
|
+
`${MODULE_NAME}.config.yaml`,
|
|
24
|
+
`${MODULE_NAME}.config.yml`,
|
|
25
|
+
`${MODULE_NAME}.config.json`,
|
|
22
26
|
];
|
|
23
|
-
const envFileNames = [".env", ".env.local"];
|
|
27
|
+
export const envFileNames = [".env", ".env.local"];
|
|
24
28
|
|
|
25
29
|
export const keyEnvVar = "APOLLO_KEY";
|
|
26
30
|
|
|
@@ -39,14 +43,31 @@ export type ConfigResult<T> = {
|
|
|
39
43
|
|
|
40
44
|
// XXX load .env files automatically
|
|
41
45
|
|
|
46
|
+
const loadJsonc: Loader = (filename, contents) => {
|
|
47
|
+
const errors: ParseError[] = [];
|
|
48
|
+
try {
|
|
49
|
+
return parseJsonC(contents, errors);
|
|
50
|
+
} finally {
|
|
51
|
+
if (errors.length) {
|
|
52
|
+
Debug.error(
|
|
53
|
+
`Error parsing JSONC file ${filename}, file might not be valid JSONC`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
42
59
|
export async function loadConfig({
|
|
43
60
|
configPath,
|
|
44
61
|
}: LoadConfigSettings): Promise<ApolloConfig | null> {
|
|
45
62
|
const explorer = cosmiconfig(MODULE_NAME, {
|
|
46
|
-
searchPlaces:
|
|
63
|
+
searchPlaces: supportedConfigFileNames,
|
|
47
64
|
loaders: {
|
|
48
65
|
...defaultLoaders,
|
|
49
|
-
|
|
66
|
+
".ts": loadTs,
|
|
67
|
+
".mjs": loadJs,
|
|
68
|
+
".cjs": loadJs,
|
|
69
|
+
".js": loadJs,
|
|
70
|
+
".json": loadJsonc,
|
|
50
71
|
},
|
|
51
72
|
});
|
|
52
73
|
|