vscode-apollo 2.3.1 → 2.3.3

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 (38) hide show
  1. package/.circleci/config.yml +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/images/marketplace/federation-directive-hover.png +0 -0
  4. package/package.json +8 -7
  5. package/sampleWorkspace/configFileTypes/cjsConfig/apollo.config.cjs +8 -0
  6. package/sampleWorkspace/configFileTypes/cjsConfig/package.json +4 -0
  7. package/sampleWorkspace/configFileTypes/cjsConfig/src/test.js +8 -0
  8. package/sampleWorkspace/configFileTypes/jsConfigWithCJS/apollo.config.js +8 -0
  9. package/sampleWorkspace/configFileTypes/jsConfigWithCJS/package.json +4 -0
  10. package/sampleWorkspace/configFileTypes/jsConfigWithCJS/src/test.js +8 -0
  11. package/sampleWorkspace/configFileTypes/jsConfigWithESM/apollo.config.js +8 -0
  12. package/sampleWorkspace/configFileTypes/jsConfigWithESM/package.json +4 -0
  13. package/sampleWorkspace/configFileTypes/jsConfigWithESM/src/test.js +8 -0
  14. package/sampleWorkspace/configFileTypes/mjsConfig/apollo.config.mjs +8 -0
  15. package/sampleWorkspace/configFileTypes/mjsConfig/package.json +4 -0
  16. package/sampleWorkspace/configFileTypes/mjsConfig/src/test.js +8 -0
  17. package/sampleWorkspace/configFileTypes/tsConfigWithCJS/apollo.config.ts +8 -0
  18. package/sampleWorkspace/configFileTypes/tsConfigWithCJS/package.json +4 -0
  19. package/sampleWorkspace/configFileTypes/tsConfigWithCJS/src/test.js +8 -0
  20. package/sampleWorkspace/configFileTypes/tsConfigWithESM/apollo.config.ts +8 -0
  21. package/sampleWorkspace/configFileTypes/tsConfigWithESM/package.json +4 -0
  22. package/sampleWorkspace/configFileTypes/tsConfigWithESM/src/test.js +8 -0
  23. package/sampleWorkspace/sampleWorkspace.code-workspace +3 -0
  24. package/schemas/apollo.config.schema.json +57 -7
  25. package/src/__e2e__/runTests.js +0 -1
  26. package/src/build.js +2 -2
  27. package/src/language-server/__e2e__/configFileTypes.e2e.ts +37 -0
  28. package/src/language-server/__e2e__/utils.ts +2 -1
  29. package/src/language-server/config/__tests__/loadConfig.ts +28 -47
  30. package/src/language-server/config/cache-busting-resolver.js +4 -12
  31. package/src/language-server/config/cache-busting-resolver.types.ts +1 -1
  32. package/src/language-server/config/config.ts +122 -98
  33. package/src/language-server/config/loadConfig.ts +3 -1
  34. package/src/language-server/config/loadTsConfig.ts +33 -16
  35. package/src/language-server/project/rover/DocumentSynchronization.ts +2 -2
  36. package/src/language-server/project/rover/project.ts +47 -20
  37. package/src/language-server/server.ts +7 -1
  38. package/src/language-server/utilities/debug.ts +49 -8
@@ -6,7 +6,7 @@ orbs:
6
6
  executors:
7
7
  node:
8
8
  docker:
9
- - image: cimg/node:22.8.0
9
+ - image: cimg/node:22.9.0
10
10
  working_directory: ~/vscode-graphql
11
11
 
12
12
  commands:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#191](https://github.com/apollographql/vscode-graphql/pull/191) [`2e56f42d`](https://github.com/apollographql/vscode-graphql/commit/2e56f42d172a7ec8afd003f056aeabac9eab1789) Thanks [@svc-secops](https://github.com/svc-secops)! - Chores: update various dependencies (#181, #191, #217, #218)
8
+
9
+ ## 2.3.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#211](https://github.com/apollographql/vscode-graphql/pull/211) [`9aa1fc1b`](https://github.com/apollographql/vscode-graphql/commit/9aa1fc1b9b419a5e72216f032e64aa5f86f15b59) Thanks [@phryneas](https://github.com/phryneas)! - Avoid detection if .js config file is ESM or CommonJs, just try both.
14
+
3
15
  ## 2.3.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "vscode-apollo",
3
3
  "displayName": "Apollo GraphQL",
4
4
  "description": "Rich editor support for GraphQL client and server development that seamlessly integrates with the Apollo platform",
5
- "version": "2.3.1",
5
+ "version": "2.3.3",
6
6
  "referenceID": "87197759-7617-40d0-b32e-46d378e907c7",
7
7
  "author": "Apollo GraphQL <opensource@apollographql.com>",
8
8
  "license": "MIT",
@@ -37,9 +37,9 @@
37
37
  "vscode": "^1.90.0"
38
38
  },
39
39
  "dependencies": {
40
- "@apollo/client": "3.11.4",
41
- "@apollo/subgraph": "2.8.4",
42
- "@graphql-tools/schema": "10.0.5",
40
+ "@apollo/client": "3.11.8",
41
+ "@apollo/subgraph": "2.9.1",
42
+ "@graphql-tools/schema": "10.0.6",
43
43
  "@wry/equality": "0.5.7",
44
44
  "cosmiconfig": "9.0.0",
45
45
  "dotenv": "16.4.5",
@@ -55,6 +55,7 @@
55
55
  "lz-string": "1.5.0",
56
56
  "minimatch": "10.0.1",
57
57
  "moment": "2.30.1",
58
+ "semver": "7.6.3",
58
59
  "undici": "6.19.8",
59
60
  "vscode-languageclient": "9.0.1",
60
61
  "vscode-languageserver": "9.0.1",
@@ -62,7 +63,7 @@
62
63
  "vscode-uri": "3.0.8",
63
64
  "which": "4.0.0",
64
65
  "zod": "3.23.8",
65
- "zod-validation-error": "3.3.1"
66
+ "zod-validation-error": "3.4.0"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@apollo/rover": "0.27.0-alpha.0",
@@ -70,7 +71,7 @@
70
71
  "@changesets/cli": "2.26.2",
71
72
  "@graphql-codegen/cli": "^5.0.2",
72
73
  "@graphql-codegen/typescript-operations": "^4.2.3",
73
- "@types/jest": "29.5.12",
74
+ "@types/jest": "29.5.13",
74
75
  "@types/lodash.debounce": "4.0.9",
75
76
  "@types/lodash.merge": "4.6.9",
76
77
  "@types/lodash.throttle": "^4.1.9",
@@ -89,7 +90,7 @@
89
90
  "import-fresh": "^3.3.0",
90
91
  "jest": "29.7.0",
91
92
  "jest-environment-node": "29.7.0",
92
- "memfs": "4.11.1",
93
+ "memfs": "4.11.2",
93
94
  "npm-run-all": "^4.1.5",
94
95
  "prettier": "3.0.3",
95
96
  "rimraf": "6.0.1",
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ client: {
3
+ service: {
4
+ name: "cjsConfig",
5
+ localSchemaFile: "./starwarsSchema.graphql",
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,8 @@
1
+ import gql from "graphql-tag";
2
+ gql`
3
+ query Test {
4
+ droid(id: "2000") {
5
+ name
6
+ }
7
+ }
8
+ `;
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ client: {
3
+ service: {
4
+ name: "jsConfigWithCJS",
5
+ localSchemaFile: "./starwarsSchema.graphql",
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,8 @@
1
+ import gql from "graphql-tag";
2
+ gql`
3
+ query Test {
4
+ droid(id: "2000") {
5
+ name
6
+ }
7
+ }
8
+ `;
@@ -0,0 +1,8 @@
1
+ export default {
2
+ client: {
3
+ service: {
4
+ name: "jsConfigWithESM",
5
+ localSchemaFile: "./starwarsSchema.graphql",
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test",
3
+ "type": "commonjs"
4
+ }
@@ -0,0 +1,8 @@
1
+ import gql from "graphql-tag";
2
+ gql`
3
+ query Test {
4
+ droid(id: "2000") {
5
+ name
6
+ }
7
+ }
8
+ `;
@@ -0,0 +1,8 @@
1
+ export default {
2
+ client: {
3
+ service: {
4
+ name: "mjsConfig",
5
+ localSchemaFile: "./starwarsSchema.graphql",
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test",
3
+ "type": "commonjs"
4
+ }
@@ -0,0 +1,8 @@
1
+ import gql from "graphql-tag";
2
+ gql`
3
+ query Test {
4
+ droid(id: "2000") {
5
+ name
6
+ }
7
+ }
8
+ `;
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ client: {
3
+ service: {
4
+ name: "tsConfigWithCJS",
5
+ localSchemaFile: "./starwarsSchema.graphql",
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test",
3
+ "type": "module"
4
+ }
@@ -0,0 +1,8 @@
1
+ import gql from "graphql-tag";
2
+ gql`
3
+ query Test {
4
+ droid(id: "2000") {
5
+ name
6
+ }
7
+ }
8
+ `;
@@ -0,0 +1,8 @@
1
+ export default {
2
+ client: {
3
+ service: {
4
+ name: "tsConfigWithESM",
5
+ localSchemaFile: "./starwarsSchema.graphql",
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "test",
3
+ "type": "commonjs"
4
+ }
@@ -0,0 +1,8 @@
1
+ import gql from "graphql-tag";
2
+ gql`
3
+ query Test {
4
+ droid(id: "2000") {
5
+ name
6
+ }
7
+ }
8
+ `;
@@ -18,6 +18,9 @@
18
18
  {
19
19
  "path": "rover"
20
20
  },
21
+ {
22
+ "path": "configFileTypes"
23
+ },
21
24
  {
22
25
  "path": "../src/language-server/__tests__/fixtures/documents"
23
26
  }
@@ -4,14 +4,31 @@
4
4
  "$ref": "#/definitions/baseConfig"
5
5
  },
6
6
  {
7
- "type": "object",
8
- "properties": {
9
- "client": {
10
- "$ref": "#/definitions/clientConfig"
7
+ "anyOf": [
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "client": {
12
+ "$ref": "#/definitions/clientConfig"
13
+ }
14
+ },
15
+ "required": [
16
+ "client"
17
+ ],
18
+ "additionalProperties": false
19
+ },
20
+ {
21
+ "type": "object",
22
+ "properties": {
23
+ "rover": {
24
+ "$ref": "#/definitions/roverConfig"
25
+ }
26
+ },
27
+ "required": [
28
+ "rover"
29
+ ],
30
+ "additionalProperties": false
11
31
  }
12
- },
13
- "required": [
14
- "client"
15
32
  ]
16
33
  }
17
34
  ],
@@ -146,6 +163,36 @@
146
163
  "additionalProperties": false,
147
164
  "description": "Configuration for a Client project."
148
165
  },
166
+ "roverConfig": {
167
+ "type": "object",
168
+ "properties": {
169
+ "bin": {
170
+ "type": "string",
171
+ "description": "The path to your Rover binary. If omitted, will look in PATH."
172
+ },
173
+ "profile": {
174
+ "type": "string",
175
+ "description": "The name of the profile to use."
176
+ },
177
+ "supergraphConfig": {
178
+ "type": [
179
+ "string",
180
+ "null"
181
+ ],
182
+ "description": "The path to your `supergraph.yaml` file. \nDefaults to a `supergraph.yaml` in the folder of your `apollo.config.json`, if there is one."
183
+ },
184
+ "extraArgs": {
185
+ "type": "array",
186
+ "items": {
187
+ "type": "string"
188
+ },
189
+ "default": [],
190
+ "description": "Extra arguments to pass to the Rover CLI."
191
+ }
192
+ },
193
+ "additionalProperties": false,
194
+ "description": "Configuration for a federated project."
195
+ },
149
196
  "engineConfig": {
150
197
  "type": "object",
151
198
  "properties": {
@@ -173,6 +220,9 @@
173
220
  "client": {
174
221
  "description": "Configuration for a Client project."
175
222
  },
223
+ "rover": {
224
+ "description": "Configuration for a federated project."
225
+ },
176
226
  "service": {
177
227
  "description": "This option is no longer supported, please remove it from your configuration file."
178
228
  }
@@ -18,7 +18,6 @@ async function main() {
18
18
  const TEST_PORT = 7096;
19
19
  process.env.APOLLO_ENGINE_ENDPOINT = "http://localhost:7096/apollo";
20
20
  process.env.MOCK_SERVER_PORT = String(TEST_PORT);
21
- process.env.APOLLO_FEATURE_FLAGS = "rover";
22
21
  disposables.push(
23
22
  ...(await Promise.all([
24
23
  runMockServer(TEST_PORT, false, loadDefaultMocks),
package/src/build.js CHANGED
@@ -68,7 +68,7 @@ const buildJsonSchemaPlugin = {
68
68
  const {
69
69
  configSchema,
70
70
  clientConfig,
71
- // roverConfig,
71
+ roverConfig,
72
72
  engineConfig,
73
73
  baseConfig,
74
74
  // @ts-ignore
@@ -78,7 +78,7 @@ const buildJsonSchemaPlugin = {
78
78
  errorMessages: true,
79
79
  definitions: {
80
80
  clientConfig,
81
- //roverConfig,
81
+ roverConfig,
82
82
  engineConfig,
83
83
  baseConfig,
84
84
  },
@@ -0,0 +1,37 @@
1
+ import { writeFile } from "fs/promises";
2
+ import {
3
+ reloadService,
4
+ waitForLSP,
5
+ resolveRelativeToSampleWorkspace,
6
+ } from "./utils";
7
+
8
+ test.each([
9
+ ["cjsConfig", "commonjs"],
10
+ ["cjsConfig", "module"],
11
+ ["mjsConfig", "module"],
12
+ ["mjsConfig", "commonjs"],
13
+ ["jsConfigWithCJS", "commonjs"],
14
+ ["jsConfigWithCJS", "module"],
15
+ ["jsConfigWithESM", "module"],
16
+ ["jsConfigWithESM", "commonjs"],
17
+ ["tsConfigWithCJS", "commonjs"],
18
+ ["tsConfigWithCJS", "module"],
19
+ ["tsConfigWithESM", "module"],
20
+ ["tsConfigWithESM", "commonjs"],
21
+ ] as const)("%s with `type: '%s'`", async (project, moduleType) => {
22
+ await writeFile(
23
+ resolveRelativeToSampleWorkspace(`configFileTypes/${project}/package.json`),
24
+ JSON.stringify(
25
+ {
26
+ name: "test",
27
+ type: moduleType,
28
+ },
29
+ undefined,
30
+ 2,
31
+ ),
32
+ "utf-8",
33
+ );
34
+ await reloadService();
35
+ const stats = await waitForLSP(`configFileTypes/${project}/src/test.js`);
36
+ expect(stats.serviceId).toBe(project);
37
+ });
@@ -7,6 +7,7 @@ import { VSCodeGraphQLExtension } from "src/extension";
7
7
  function resolve(file: string) {
8
8
  return join(__dirname, "..", "..", "..", "sampleWorkspace", file);
9
9
  }
10
+ export { resolve as resolveRelativeToSampleWorkspace };
10
11
 
11
12
  export type GetPositionFn = ReturnType<typeof getPositionForEditor>;
12
13
  export function getPositionForEditor(editor: vscode.TextEditor) {
@@ -65,7 +66,7 @@ export function waitForLSP(file: string) {
65
66
  uri.toString(),
66
67
  );
67
68
  expect(stats.loaded).toBe(true);
68
- return stats;
69
+ return stats as ProjectStats & { loaded: true };
69
70
  });
70
71
  }
71
72
 
@@ -1,24 +1,8 @@
1
- let { loadConfig } = require("../");
2
- let { ClientConfig, RoverConfig } = require("../config");
1
+ import { loadConfig } from "../";
2
+ import { ClientConfig, RoverConfig } from "../config";
3
3
  import * as path from "path";
4
4
  import * as fs from "fs";
5
5
 
6
- async function withFeatureFlags(flags: string, fn: () => void) {
7
- const FF = process.env.APOLLO_FEATURE_FLAGS;
8
- try {
9
- process.env.APOLLO_FEATURE_FLAGS = flags;
10
- jest.resetModules();
11
- ({ loadConfig } = require("../"));
12
- ({ ClientConfig, RoverConfig } = require("../config"));
13
- return await fn();
14
- } finally {
15
- process.env.APOLLO_FEATURE_FLAGS = FF;
16
- jest.resetModules();
17
- ({ loadConfig } = require("../"));
18
- ({ ClientConfig, RoverConfig } = require("../config"));
19
- }
20
- }
21
-
22
6
  const makeNestedDir = (dir: string) => {
23
7
  if (fs.existsSync(dir)) return;
24
8
 
@@ -109,25 +93,24 @@ Object {
109
93
  `);
110
94
  });
111
95
 
112
- it("loads with rover defaults from different dir", () =>
113
- withFeatureFlags("rover", async () => {
114
- writeFilesToDir(dir, {
115
- "apollo.config.js": `
96
+ it("loads with rover defaults from different dir", async () => {
97
+ writeFilesToDir(dir, {
98
+ "apollo.config.js": `
116
99
  module.exports = {
117
100
  rover: {
118
101
  }
119
102
  }
120
103
  `,
104
+ });
105
+ fs.mkdirSync(`${dir}/bin`);
106
+ fs.writeFileSync(`${dir}/bin/rover`, "", { mode: 0o755 });
107
+ let oldPath = process.env.PATH;
108
+ process.env.PATH = `${dir}/bin:${oldPath}`;
109
+ try {
110
+ const config = await loadConfig({
111
+ configPath: dirPath,
121
112
  });
122
- fs.mkdirSync(`${dir}/bin`);
123
- fs.writeFileSync(`${dir}/bin/rover`, "", { mode: 0o755 });
124
- let oldPath = process.env.PATH;
125
- process.env.PATH = `${dir}/bin:${oldPath}`;
126
- try {
127
- const config = await loadConfig({
128
- configPath: dirPath,
129
- });
130
- expect(config?.rawConfig).toMatchInlineSnapshot(`
113
+ expect(config?.rawConfig).toMatchInlineSnapshot(`
131
114
  Object {
132
115
  "engine": Object {
133
116
  "endpoint": "https://graphql.api.apollographql.com/api/graphql",
@@ -138,10 +121,10 @@ Object {
138
121
  },
139
122
  }
140
123
  `);
141
- } finally {
142
- process.env.PATH = oldPath;
143
- }
144
- }));
124
+ } finally {
125
+ process.env.PATH = oldPath;
126
+ }
127
+ });
145
128
 
146
129
  it("[deprecated] loads config from package.json", async () => {
147
130
  writeFilesToDir(dir, {
@@ -280,7 +263,6 @@ client:
280
263
 
281
264
  await loadConfig({
282
265
  configPath: dirPath,
283
- requireConfig: true, // this is what we're testing
284
266
  });
285
267
 
286
268
  expect(spy).toHaveBeenCalledWith(
@@ -299,7 +281,7 @@ client:
299
281
  }).catch((e: any) => e);
300
282
 
301
283
  expect(error.message).toMatch(
302
- /Config needs to contain a 'client' field./i,
284
+ /Config needs to contain either 'client' or 'rover' fields/i,
303
285
  );
304
286
  });
305
287
  });
@@ -374,18 +356,17 @@ client:
374
356
  expect(config).toBeInstanceOf(ClientConfig);
375
357
  });
376
358
 
377
- it("infers rover projects from config", () =>
378
- withFeatureFlags("rover", async () => {
379
- writeFilesToDir(dir, {
380
- "apollo.config.js": `module.exports = { rover: { bin: "/usr/bin/env" } }`,
381
- });
359
+ it("infers rover projects from config", async () => {
360
+ writeFilesToDir(dir, {
361
+ "apollo.config.js": `module.exports = { rover: { bin: "/usr/bin/env" } }`,
362
+ });
382
363
 
383
- const config = await loadConfig({
384
- configPath: dirPath,
385
- });
364
+ const config = await loadConfig({
365
+ configPath: dirPath,
366
+ });
386
367
 
387
- expect(config).toBeInstanceOf(RoverConfig);
388
- }));
368
+ expect(config).toBeInstanceOf(RoverConfig);
369
+ });
389
370
  });
390
371
 
391
372
  describe("service name", () => {
@@ -24,20 +24,12 @@ async function resolve(specifier, context, nextResolve) {
24
24
  if (context.importAttributes.as !== "cachebust") {
25
25
  return nextResolve(specifier, context);
26
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);
27
+ // no need to resolve at all, we have all necessary information
37
28
  return {
38
- ...result,
39
- url: bustFileName(result.url),
29
+ url: bustFileName(specifier),
30
+ format: context.importAttributes.format,
40
31
  importAttributes: context.importAttributes,
32
+ shortCircuit: true,
41
33
  };
42
34
  }
43
35
 
@@ -4,7 +4,7 @@ export type ImportAttributes =
4
4
  | {
5
5
  as: "cachebust";
6
6
  contents: string;
7
- format?: Format;
7
+ format: Format;
8
8
  }
9
9
  | { as?: undefined };
10
10
 
@@ -9,10 +9,9 @@ import which from "which";
9
9
  import { accessSync, constants as fsConstants, statSync } from "node:fs";
10
10
  import { AsyncLocalStorage } from "async_hooks";
11
11
  import { existsSync } from "fs";
12
-
13
- const ROVER_AVAILABLE = (process.env.APOLLO_FEATURE_FLAGS || "")
14
- .split(",")
15
- .includes("rover");
12
+ import { spawn } from "node:child_process";
13
+ import { text } from "node:stream/consumers";
14
+ import semver from "semver";
16
15
 
17
16
  function ignoredFieldWarning(
18
17
  getMessage = (path: string) =>
@@ -143,49 +142,53 @@ export const clientConfig = z
143
142
  .describe("Configuration for a Client project.");
144
143
  export type ClientConfigFormat = z.infer<typeof clientConfig>;
145
144
 
146
- export const roverConfig = z.object({
147
- bin: z
148
- .preprocess(
149
- (val) => val || which.sync("rover", { nothrow: true }) || undefined,
150
- z.string({
151
- message:
152
- "Rover binary not found. Please either install it system-wide in PATH, or provide the `bin` option. Also ensure that the binary is executable.",
153
- }),
154
- )
155
- .refine(
156
- (bin) => {
157
- try {
158
- // is executable?
159
- accessSync(bin, fsConstants.X_OK);
160
- // is a file and not a directory?
161
- return statSync(bin).isFile();
162
- } catch {
163
- return false;
164
- }
165
- },
166
- {
167
- message:
168
- "Rover binary is not marked as an executable. If you are using OS X or Linux, ensure to set the executable bit.",
169
- },
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."),
173
- supergraphConfig: z
174
- .preprocess((value) => {
175
- if (value !== undefined) return value;
176
- const configPath = contextStore.getStore()?.configPath!;
177
- const supergraphConfig = join(configPath, "supergraph.yaml");
178
- return existsSync(supergraphConfig) ? supergraphConfig : undefined;
179
- }, z.string().nullable().optional())
180
- .describe(
181
- "The path to your `supergraph.yaml` file. \n" +
182
- "Defaults to a `supergraph.yaml` in the folder of your `apollo.config.js`, if there is one.",
183
- ),
184
- extraArgs: z
185
- .array(z.string())
186
- .default([])
187
- .describe("Extra arguments to pass to the Rover CLI."),
188
- });
145
+ export const roverConfig = z
146
+ .object({
147
+ bin: z
148
+ .preprocess(
149
+ (val) => val || which.sync("rover", { nothrow: true }) || undefined,
150
+ z.string({
151
+ message:
152
+ "Rover binary not found. Please either install it system-wide in PATH, or provide the `bin` option. Also ensure that the binary is executable.",
153
+ }),
154
+ )
155
+ .refine(
156
+ (bin) => {
157
+ try {
158
+ // is executable?
159
+ accessSync(bin, fsConstants.X_OK);
160
+ // is a file and not a directory?
161
+ return statSync(bin).isFile();
162
+ } catch {
163
+ return false;
164
+ }
165
+ },
166
+ {
167
+ message:
168
+ "Rover binary is not marked as an executable. If you are using OS X or Linux, ensure to set the executable bit.",
169
+ },
170
+ )
171
+ .describe(
172
+ "The path to your Rover binary. If omitted, will look in PATH.",
173
+ ),
174
+ profile: z.string().optional().describe("The name of the profile to use."),
175
+ supergraphConfig: z
176
+ .preprocess((value) => {
177
+ if (value !== undefined) return value;
178
+ const configPath = contextStore.getStore()?.configPath || ".";
179
+ const supergraphConfig = join(configPath, "supergraph.yaml");
180
+ return existsSync(supergraphConfig) ? supergraphConfig : undefined;
181
+ }, z.string().nullable().optional())
182
+ .describe(
183
+ "The path to your `supergraph.yaml` file. \n" +
184
+ "Defaults to a `supergraph.yaml` in the folder of your `apollo.config.json`, if there is one.",
185
+ ),
186
+ extraArgs: z
187
+ .array(z.string())
188
+ .default([])
189
+ .describe("Extra arguments to pass to the Rover CLI."),
190
+ })
191
+ .describe("Configuration for a federated project.");
189
192
  type RoverConfigFormat = z.infer<typeof roverConfig>;
190
193
 
191
194
  export const engineConfig = z
@@ -212,12 +215,7 @@ export type EngineConfig = z.infer<typeof engineConfig>;
212
215
  export const baseConfig = z.object({
213
216
  engine: engineConfig.default({}),
214
217
  client: z.unknown().optional().describe(clientConfig.description!),
215
- ...ifRoverAvailable(
216
- {
217
- rover: z.unknown().optional(),
218
- },
219
- {},
220
- ),
218
+ rover: z.unknown().optional().describe(roverConfig.description!),
221
219
  service: ignoredFieldWarning(
222
220
  (path) =>
223
221
  `Service-type projects are no longer supported. Please remove the "${path}" field from your configuration file.`,
@@ -234,56 +232,36 @@ export type FullRoverConfigFormat = Extract<
234
232
  { rover: {} }
235
233
  >;
236
234
 
237
- /** Helper function for TypeScript - we just want the first type to make it into the types, not the "no feature flag" fallback */
238
- function ifRoverAvailable<T>(yes: T, no: any): T {
239
- return ROVER_AVAILABLE ? yes : no;
240
- }
241
-
242
235
  export const configSchema = baseConfig
243
236
  .superRefine((val, ctx) => {
244
- if (ROVER_AVAILABLE) {
245
- if ("client" in val && "rover" in val) {
246
- ctx.addIssue({
247
- code: "custom",
248
- message: "Config cannot contain both 'client' and 'rover' fields",
249
- fatal: true,
250
- });
251
- }
252
- if (!("client" in val) && !("rover" in val)) {
253
- ctx.addIssue({
254
- code: "custom",
255
- message: "Config needs to contain either 'client' or 'rover' fields",
256
- fatal: true,
257
- });
258
- }
259
- } else {
260
- if (!("client" in val)) {
261
- ctx.addIssue({
262
- code: "custom",
263
- message: "Config needs to contain a 'client' field.",
264
- fatal: true,
265
- });
266
- }
237
+ if ("client" in val && "rover" in val) {
238
+ ctx.addIssue({
239
+ code: "custom",
240
+ message: "Config cannot contain both 'client' and 'rover' fields",
241
+ fatal: true,
242
+ });
243
+ }
244
+ if (!("client" in val) && !("rover" in val)) {
245
+ ctx.addIssue({
246
+ code: "custom",
247
+ message: "Config needs to contain either 'client' or 'rover' fields",
248
+ fatal: true,
249
+ });
267
250
  }
268
251
  })
269
252
  .and(
270
- ifRoverAvailable(
271
- z.union([
272
- z
273
- .object({
274
- client: clientConfig,
275
- })
276
- .transform((val): typeof val & { rover?: never } => val),
277
- z
278
- .object({
279
- rover: roverConfig,
280
- })
281
- .transform((val): typeof val & { client?: never } => val),
282
- ]),
283
- z.object({
284
- client: clientConfig,
285
- }),
286
- ),
253
+ z.union([
254
+ z
255
+ .object({
256
+ client: clientConfig,
257
+ })
258
+ .transform((val): typeof val & { rover?: never } => val),
259
+ z
260
+ .object({
261
+ rover: roverConfig,
262
+ })
263
+ .transform((val): typeof val & { client?: never } => val),
264
+ ]),
287
265
  );
288
266
  export type RawApolloConfigFormat = z.input<typeof configSchema>;
289
267
  export type ParsedApolloConfigFormat = z.output<typeof configSchema>;
@@ -371,6 +349,11 @@ export abstract class ApolloConfig {
371
349
  if (this._graphId) return this._graphId;
372
350
  return getGraphIdFromConfig(this.rawConfig);
373
351
  }
352
+
353
+ /**
354
+ * execute some additional asynchronous verification steps that cannot be part of the sync parsing part
355
+ */
356
+ async verify() {}
374
357
  }
375
358
 
376
359
  export class ClientConfig extends ApolloConfig {
@@ -395,4 +378,45 @@ export class RoverConfig extends ApolloConfig {
395
378
  super(rawConfig, configURI);
396
379
  this.rover = rawConfig.rover;
397
380
  }
381
+
382
+ /**
383
+ * execute some additional asynchronous verification steps that cannot be part of the sync parsing part
384
+ */
385
+ async verify() {
386
+ try {
387
+ const child = spawn(this.rover.bin, ["-V"], {
388
+ stdio: ["pipe", "pipe", "ignore"],
389
+ });
390
+ const output = await text(child.stdout);
391
+ const versionPrefix = "Rover ";
392
+ if (output.startsWith(versionPrefix)) {
393
+ const version = output.slice(versionPrefix.length).trim();
394
+ if (!semver.valid(version)) {
395
+ // not a valid version, we accept this and will try anyways
396
+ return;
397
+ }
398
+ if (semver.gte(version, "0.27.0-alpha.0")) {
399
+ // this is a supported version
400
+ return;
401
+ }
402
+ const error = new Error(
403
+ `Rover version ${version} is not supported by the extension. Please upgrade to at least 0.27.0.`,
404
+ );
405
+ // @ts-expect-error would require a target of ES2022 in tsconfig
406
+ error.cause = "ROVER_TOO_OLD";
407
+ throw error;
408
+ }
409
+ // we can't find out the version, but we'll try anyways
410
+ } catch (error) {
411
+ if (
412
+ error &&
413
+ error instanceof Error &&
414
+ // @ts-expect-error would require a target of ES2022 in tsconfig
415
+ error.cause === "ROVER_TOO_OLD"
416
+ ) {
417
+ throw error;
418
+ }
419
+ // we ignore all other errors and will handle that when we actually spawn the rover binary
420
+ }
421
+ }
398
422
  }
@@ -121,9 +121,11 @@ export async function loadConfig({
121
121
 
122
122
  let { config, filepath } = loadedConfig;
123
123
 
124
- return parseApolloConfig(config, URI.file(resolve(filepath)), {
124
+ const finalConfig = parseApolloConfig(config, URI.file(resolve(filepath)), {
125
125
  apiKey,
126
126
  serviceName: nameFromKey,
127
127
  configPath: dirname(filepath),
128
128
  });
129
+ await finalConfig.verify();
130
+ return finalConfig;
129
131
  }
@@ -1,5 +1,5 @@
1
1
  import { Loader } from "cosmiconfig";
2
- import { dirname } from "node:path";
2
+ import { dirname, extname } from "node:path";
3
3
  import typescript from "typescript";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { register } from "node:module";
@@ -59,19 +59,7 @@ async function load(
59
59
  error.message = `TypeScript Error in ${filepath}:\n${error.message}`;
60
60
  throw error;
61
61
  }
62
- // eslint-disable-next-line @typescript-eslint/return-await
63
- const imported = await import(
64
- filepath,
65
- //@ts-ignore
66
- {
67
- with: {
68
- as: "cachebust",
69
- contents: transpiledContent,
70
- format: type,
71
- } satisfies ImportAttributes,
72
- }
73
- );
74
- return imported.default;
62
+ return loadCachebustedJs(filepath, transpiledContent, type);
75
63
  }
76
64
 
77
65
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -92,15 +80,44 @@ function resolveTsConfig(directory: string): any {
92
80
  }
93
81
 
94
82
  export const loadJs: Loader = async function loadJs(filepath, contents) {
83
+ const extension = extname(filepath);
84
+ if (extension === ".mjs") {
85
+ return loadCachebustedJs(filepath, contents, "module");
86
+ }
87
+ if (extension === ".cjs") {
88
+ return loadCachebustedJs(filepath, contents, "commonjs");
89
+ }
90
+ try {
91
+ return await loadCachebustedJs(filepath, contents, "module");
92
+ } catch (error) {
93
+ if (
94
+ error instanceof Error &&
95
+ // [ERROR] ReferenceError: module is not defined in ES module scope
96
+ error.message.includes("module is not defined")
97
+ ) {
98
+ return loadCachebustedJs(filepath, contents, "commonjs");
99
+ } else {
100
+ throw error;
101
+ }
102
+ }
103
+ };
104
+
105
+ async function loadCachebustedJs(
106
+ filename: string,
107
+ contents: string,
108
+ type: "module" | "commonjs",
109
+ ) {
95
110
  return (
96
111
  await import(
97
- filepath, // @ts-ignore
112
+ filename,
113
+ // @ts-ignore
98
114
  {
99
115
  with: {
100
116
  as: "cachebust",
101
117
  contents,
118
+ format: type,
102
119
  } satisfies ImportAttributes,
103
120
  }
104
121
  )
105
122
  ).default;
106
- };
123
+ }
@@ -23,7 +23,7 @@ import {
23
23
  rangeInContainingDocument,
24
24
  } from "../../utilities/source";
25
25
  import { URI } from "vscode-uri";
26
- import { DEBUG } from "./project";
26
+ import { Debug } from "../../utilities";
27
27
 
28
28
  export interface FilePart {
29
29
  fractionalIndex: string;
@@ -285,7 +285,7 @@ export class DocumentSynchronization {
285
285
  }
286
286
 
287
287
  handlePartDiagnostics(params: PublishDiagnosticsParams) {
288
- DEBUG && console.log("Received diagnostics", params);
288
+ Debug.traceVerbose("Received diagnostics", params);
289
289
  const uriDetails = splitUri(params.uri);
290
290
  const found = this.knownFiles.get(uriDetails.uri);
291
291
  if (!found || found.source === "lsp") {
@@ -36,8 +36,8 @@ import { VSCodeConnection } from "../../server";
36
36
  import { getLanguageIdForExtension } from "../../utilities/languageIdForExtension";
37
37
  import { extname } from "node:path";
38
38
  import type { FileExtension } from "../../../tools/utilities/languageInformation";
39
-
40
- export const DEBUG = true;
39
+ import { Debug } from "../../utilities";
40
+ import { TraceLevel } from "src/language-server/utilities/debug";
41
41
 
42
42
  export function isRoverConfig(config: ApolloConfig): config is RoverConfig {
43
43
  return config instanceof RoverConfig;
@@ -115,11 +115,14 @@ export class RoverProject extends GraphQLProject {
115
115
  params?: P,
116
116
  ): Promise<void> {
117
117
  const connection = await this.getConnection();
118
- DEBUG &&
119
- console.log("sending notification %o", {
118
+ Debug.traceMessage(
119
+ "[Rover] Sending notification: " + type.method,
120
+ "[Rover] Sending notification %o",
121
+ {
120
122
  type: type.method,
121
123
  params,
122
- });
124
+ },
125
+ );
123
126
  try {
124
127
  return await connection.sendNotification(type, params);
125
128
  } catch (error) {
@@ -136,10 +139,20 @@ export class RoverProject extends GraphQLProject {
136
139
  token?: CancellationToken,
137
140
  ): Promise<R> {
138
141
  const connection = await this.getConnection();
139
- DEBUG && console.log("sending request %o", { type: type.method, params });
142
+ Debug.traceMessage(
143
+ "[Rover] Sending request: " + type.method,
144
+ "[Rover] Sending request %o",
145
+ { type: type.method, params },
146
+ );
140
147
  try {
141
148
  const result = await connection.sendRequest(type, params, token);
142
- DEBUG && console.log({ result });
149
+ Debug.traceMessage(
150
+ "[Rover] Received response: " + type.method,
151
+ "[Rover] Received response %s\nResult: %o",
152
+ type.method,
153
+
154
+ result,
155
+ );
143
156
  return result;
144
157
  } catch (error) {
145
158
  if (error instanceof Error) {
@@ -166,27 +179,35 @@ export class RoverProject extends GraphQLProject {
166
179
  if (this.config.rover.supergraphConfig) {
167
180
  args.push("--supergraph-config", this.config.rover.supergraphConfig);
168
181
  }
182
+ if (Debug.traceLevel >= TraceLevel.verbose) {
183
+ args.push("--log", "debug");
184
+ }
169
185
  args.push(...this.config.rover.extraArgs);
170
186
 
171
- DEBUG &&
172
- console.log(`starting ${this.config.rover.bin} '${args.join("' '")}'`);
187
+ Debug.traceVerbose(
188
+ `starting ${this.config.rover.bin} '${args.join("' '")}'`,
189
+ );
173
190
  const child = cp.spawn(this.config.rover.bin, args, {
174
- env: DEBUG ? { RUST_BACKTRACE: "1" } : {},
175
- stdio: ["pipe", "pipe", DEBUG ? "inherit" : "ignore"],
191
+ env: { NO_COLOR: "1" },
192
+ stdio: [
193
+ "pipe",
194
+ "pipe",
195
+ Debug.traceLevel >= TraceLevel.verbose ? "inherit" : "ignore",
196
+ ],
176
197
  });
177
198
  this.child = child;
178
199
  const reader = new StreamMessageReader(child.stdout);
179
200
  const writer = new StreamMessageWriter(child.stdin);
180
201
  const connection = createProtocolConnection(reader, writer);
181
202
  connection.onClose(() => {
182
- DEBUG && console.log("Connection closed");
203
+ Debug.traceMessage("[Rover] Connection closed");
183
204
  child.kill();
184
205
  source.cancel();
185
206
  this._connection = undefined;
186
207
  });
187
208
 
188
209
  connection.onError((err) => {
189
- console.error({ err });
210
+ Debug.error("%o", { err });
190
211
  });
191
212
 
192
213
  connection.onNotification(
@@ -195,11 +216,14 @@ export class RoverProject extends GraphQLProject {
195
216
  );
196
217
 
197
218
  connection.onUnhandledNotification((notification) => {
198
- DEBUG && console.info("unhandled notification from LSP", notification);
219
+ Debug.traceVerbose(
220
+ "[Rover] unhandled notification from LSP",
221
+ notification,
222
+ );
199
223
  });
200
224
 
201
225
  connection.listen();
202
- DEBUG && console.log("Initializing connection");
226
+ Debug.traceMessage("[Rover] Initializing connection");
203
227
 
204
228
  const source = new CancellationTokenSource();
205
229
  try {
@@ -213,7 +237,11 @@ export class RoverProject extends GraphQLProject {
213
237
  source.token,
214
238
  );
215
239
  this.roverCapabilities = status.capabilities;
216
- DEBUG && console.log("Connection initialized", status);
240
+ Debug.traceMessage(
241
+ "[Rover] Connection initialized",
242
+ "[Rover] Connection initialized %o",
243
+ status,
244
+ );
217
245
 
218
246
  await this.connectionStorage.run(
219
247
  connection,
@@ -222,7 +250,7 @@ export class RoverProject extends GraphQLProject {
222
250
 
223
251
  return connection;
224
252
  } catch (error) {
225
- console.error("Connection failed to initialize", error);
253
+ Debug.error("Connection with Rover failed to initialize", error);
226
254
  throw error;
227
255
  }
228
256
  }
@@ -295,7 +323,7 @@ export class RoverProject extends GraphQLProject {
295
323
  if (isRequestType(SemanticTokensRequest.type, type, params)) {
296
324
  return this.documents.getFullSemanticTokens(params, token);
297
325
  } else {
298
- DEBUG && console.info("unhandled request from VSCode", { type, params });
326
+ Debug.traceVerbose("unhandled request from VSCode", { type, params });
299
327
  return undefined;
300
328
  }
301
329
  };
@@ -304,8 +332,7 @@ export class RoverProject extends GraphQLProject {
304
332
  type,
305
333
  params,
306
334
  ) => {
307
- DEBUG &&
308
- console.info("unhandled notification from VSCode", { type, params });
335
+ Debug.traceVerbose("unhandled notification from VSCode", { type, params });
309
336
  };
310
337
 
311
338
  async onVSCodeConnectionInitialized(connection: VSCodeConnection) {
@@ -7,6 +7,7 @@ import {
7
7
  TextDocumentSyncKind,
8
8
  SymbolInformation,
9
9
  FileEvent,
10
+ SetTraceNotification,
10
11
  } from "vscode-languageserver/node";
11
12
  import { TextDocument } from "vscode-languageserver-textdocument";
12
13
  import { type QuickPickItem } from "vscode";
@@ -92,11 +93,16 @@ workspace.onConfigFilesFound(async (params) => {
92
93
  );
93
94
  });
94
95
 
96
+ connection.onNotification(SetTraceNotification.type, ({ value }) => {
97
+ Debug.traceLevel = value;
98
+ });
99
+
95
100
  connection.onInitialize(
96
- async ({ capabilities, workspaceFolders, initializationOptions }) => {
101
+ async ({ capabilities, workspaceFolders, initializationOptions, trace }) => {
97
102
  const { languageIdExtensionMap } =
98
103
  initializationOptions as InitializationOptions;
99
104
  setLanguageIdExtensionMap(languageIdExtensionMap);
105
+ Debug.traceLevel = trace;
100
106
 
101
107
  hasWorkspaceFolderCapability = !!(
102
108
  capabilities.workspace && capabilities.workspace.workspaceFolders
@@ -1,5 +1,6 @@
1
1
  import { LanguageServerNotifications as Notifications } from "../../messages";
2
- import { Connection } from "vscode-languageserver/node";
2
+ import { Connection, TraceValues } from "vscode-languageserver/node";
3
+ import { format } from "util";
3
4
 
4
5
  /**
5
6
  * for errors (and other logs in debug mode) we want to print
@@ -15,9 +16,27 @@ const createAndTrimStackTrace = () => {
15
16
  : stack;
16
17
  };
17
18
 
18
- type Logger = (message?: any) => void;
19
+ type Logger = (message?: any, minLevel?: TraceLevel) => void;
20
+ export enum TraceLevel {
21
+ "off" = 0,
22
+ "messages" = 1,
23
+ "verbose" = 2,
24
+ }
19
25
 
20
26
  export class Debug {
27
+ private static _traceLevel: TraceLevel = TraceLevel.off;
28
+ public static get traceLevel(): TraceLevel {
29
+ return Debug._traceLevel;
30
+ }
31
+ public static set traceLevel(value: TraceValues | undefined) {
32
+ if (value === "compact") {
33
+ // we do not handle "compact" and it's not possible to set in settings, but it doesn't hurt to at least map
34
+ // it to another value
35
+ this._traceLevel = TraceLevel.messages;
36
+ } else {
37
+ this._traceLevel = TraceLevel[value || "off"];
38
+ }
39
+ }
21
40
  private static connection?: Connection;
22
41
  private static infoLogger: Logger = (message) =>
23
42
  console.log("[INFO] " + message);
@@ -67,17 +86,39 @@ export class Debug {
67
86
  if (error) Debug.errorLogger = error;
68
87
  }
69
88
 
70
- public static info(message: string) {
71
- Debug.infoLogger(message);
89
+ public static info(message: string, ...param: any[]) {
90
+ Debug.infoLogger(format(message, ...param));
72
91
  }
73
92
 
74
- public static error(message: string) {
93
+ public static error(message: string, ...param: any[]) {
75
94
  const stack = createAndTrimStackTrace();
76
- Debug.errorLogger(`${message}\n${stack}`);
95
+ Debug.errorLogger(`${format(message, ...param)}\n${stack}`);
96
+ }
97
+
98
+ public static warning(message: string, ...param: any[]) {
99
+ Debug.warningLogger(format(message, ...param));
100
+ }
101
+
102
+ public static traceMessage(
103
+ short: string,
104
+ verbose = short,
105
+ ...verboseParams: any[]
106
+ ) {
107
+ if (Debug.traceLevel >= TraceLevel.verbose) {
108
+ // directly logging to `console` because
109
+ // we don't want to send yet another notification that will be traced
110
+ console.info(verbose, ...verboseParams);
111
+ } else if (Debug.traceLevel >= TraceLevel.messages) {
112
+ console.info(short);
113
+ }
77
114
  }
78
115
 
79
- public static warning(message: string) {
80
- Debug.warningLogger(message);
116
+ public static traceVerbose(message: string, ...params: any[]) {
117
+ if (Debug.traceLevel >= TraceLevel.verbose) {
118
+ // directly logging to `console` because
119
+ // we don't want to send yet another notification that will be traced
120
+ console.info(message, ...params);
121
+ }
81
122
  }
82
123
 
83
124
  public static sendErrorTelemetry(message: string) {