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.
Files changed (37) hide show
  1. package/.circleci/config.yml +1 -17
  2. package/.github/workflows/E2E.yml +27 -0
  3. package/.github/workflows/build-prs.yml +1 -1
  4. package/CHANGELOG.md +22 -0
  5. package/README.md +71 -52
  6. package/jest.e2e.config.js +2 -0
  7. package/package.json +31 -3
  8. package/renovate.json +5 -5
  9. package/sampleWorkspace/localSchemaArray/apollo.config.json +9 -0
  10. package/sampleWorkspace/localSchemaArray/src/test.js +1 -1
  11. package/sampleWorkspace/rover/apollo.config.yaml +3 -0
  12. package/sampleWorkspace/rover/src/test.graphql +11 -10
  13. package/sampleWorkspace/rover/supergraph.yaml +0 -0
  14. package/schemas/apollo.config.schema.json +184 -0
  15. package/src/__e2e__/runTests.js +1 -0
  16. package/src/build.js +53 -1
  17. package/src/language-server/__e2e__/clientSchema.e2e.ts +58 -28
  18. package/src/language-server/__e2e__/httpSchema.e2e.ts +23 -4
  19. package/src/language-server/__e2e__/localSchema.e2e.ts +23 -4
  20. package/src/language-server/__e2e__/localSchemaArray.e2e.ts +31 -6
  21. package/src/language-server/__e2e__/rover.e2e.ts +150 -0
  22. package/src/language-server/__e2e__/studioGraph.e2e.ts +18 -6
  23. package/src/language-server/__e2e__/utils.ts +114 -13
  24. package/src/language-server/config/__tests__/loadConfig.ts +35 -2
  25. package/src/language-server/config/cache-busting-resolver.js +65 -0
  26. package/src/language-server/config/cache-busting-resolver.types.ts +45 -0
  27. package/src/language-server/config/config.ts +133 -57
  28. package/src/language-server/config/loadConfig.ts +27 -6
  29. package/src/language-server/config/loadTsConfig.ts +74 -38
  30. package/src/language-server/project/base.ts +8 -8
  31. package/src/language-server/project/rover/project.ts +6 -0
  32. package/src/language-server/server.ts +8 -7
  33. package/src/language-server/workspace.ts +2 -5
  34. package/src/languageServerClient.ts +3 -1
  35. package/sampleWorkspace/localSchemaArray/apollo.config.js +0 -12
  36. package/sampleWorkspace/rover/apollo.config.js +0 -3
  37. /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 testCompletion(
72
+ export async function getCompletionItems(
54
73
  editor: vscode.TextEditor,
55
- [line, character]: [number, number],
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(line, character, line, character);
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
- new vscode.Position(line, character),
90
+ position,
67
91
  );
68
-
69
- const labels = completions.items.slice(0, expected.length).map((item) =>
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
- expect(labels).toStrictEqual(
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
- [line, character]: [number, number],
108
+ position: vscode.Position,
86
109
  ) {
87
- editor.selection = new vscode.Selection(line, character, line, character);
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
- new vscode.Position(line, character),
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
- it("loads config from a cjs file", async () => {
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 studioServiceConfig = z.string();
40
+ const NAME_DESCRIPTION =
41
+ "The name your project will be referred to by the Apollo GraphQL extension.";
38
42
 
39
- const remoteServiceConfig = z.object({
40
- name: z.string().optional(),
41
- url: z.string(),
42
- headers: z.record(z.string()).default({}),
43
- skipSSLValidation: z.boolean().default(false),
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 localServiceConfig = z.object({
48
- name: z.string().optional(),
49
- localSchemaFile: z.union([z.string(), z.array(z.string())]),
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.preprocess(
54
- (value) => value || contextStore.getStore()?.serviceName,
55
- z.union([studioServiceConfig, remoteServiceConfig, localServiceConfig]),
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 clientConfig = z.object({
60
- service: clientServiceConfig,
61
- validationRules: z
62
- .union([
63
- z.array(z.custom<ValidationRule>()),
64
- z.function().args(z.custom<ValidationRule>()).returns(z.boolean()),
65
- ])
66
- .optional(),
67
- // maybe shared with rover?
68
- includes: z.array(z.string()).optional(),
69
- // maybe shared with rover?
70
- excludes: z.array(z.string()).default(["**/node_modules", "**/__tests__"]),
71
- // maybe shared with rover?
72
- tagName: z.string().default("gql"),
73
- // removed:
74
- clientOnlyDirectives: ignoredFieldWarning(),
75
- clientSchemaDirectives: ignoredFieldWarning(),
76
- statsWindow: ignoredFieldWarning(),
77
- name: ignoredFieldWarning(),
78
- referenceId: ignoredFieldWarning(),
79
- version: ignoredFieldWarning(),
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
- profile: z.string().optional(),
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.object({
128
- endpoint: z
129
- .string()
130
- .default(
131
- process.env.APOLLO_ENGINE_ENDPOINT ||
132
- "https://graphql.api.apollographql.com/api/graphql",
133
- ),
134
- apiKey: z.preprocess(
135
- (val) => val || contextStore.getStore()?.apiKey,
136
- z.string().optional(),
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
- rover: z.unknown().optional(),
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 { loadTs } from "./loadTsConfig";
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 defaultFileNames = [
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: defaultFileNames,
63
+ searchPlaces: supportedConfigFileNames,
47
64
  loaders: {
48
65
  ...defaultLoaders,
49
- [".ts"]: loadTs,
66
+ ".ts": loadTs,
67
+ ".mjs": loadJs,
68
+ ".cjs": loadJs,
69
+ ".js": loadJs,
70
+ ".json": loadJsonc,
50
71
  },
51
72
  });
52
73