vscode-apollo 2.3.0 → 2.3.2

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 (40) hide show
  1. package/.circleci/config.yml +0 -16
  2. package/.github/workflows/E2E.yml +27 -0
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +6 -3
  5. package/jest.e2e.config.js +2 -0
  6. package/package.json +2 -1
  7. package/sampleWorkspace/configFileTypes/cjsConfig/apollo.config.cjs +8 -0
  8. package/sampleWorkspace/configFileTypes/cjsConfig/package.json +4 -0
  9. package/sampleWorkspace/configFileTypes/cjsConfig/src/test.js +8 -0
  10. package/sampleWorkspace/configFileTypes/jsConfigWithCJS/apollo.config.js +8 -0
  11. package/sampleWorkspace/configFileTypes/jsConfigWithCJS/package.json +4 -0
  12. package/sampleWorkspace/configFileTypes/jsConfigWithCJS/src/test.js +8 -0
  13. package/sampleWorkspace/configFileTypes/jsConfigWithESM/apollo.config.js +8 -0
  14. package/sampleWorkspace/configFileTypes/jsConfigWithESM/package.json +4 -0
  15. package/sampleWorkspace/configFileTypes/jsConfigWithESM/src/test.js +8 -0
  16. package/sampleWorkspace/configFileTypes/mjsConfig/apollo.config.mjs +8 -0
  17. package/sampleWorkspace/configFileTypes/mjsConfig/package.json +4 -0
  18. package/sampleWorkspace/configFileTypes/mjsConfig/src/test.js +8 -0
  19. package/sampleWorkspace/configFileTypes/tsConfigWithCJS/apollo.config.ts +8 -0
  20. package/sampleWorkspace/configFileTypes/tsConfigWithCJS/package.json +4 -0
  21. package/sampleWorkspace/configFileTypes/tsConfigWithCJS/src/test.js +8 -0
  22. package/sampleWorkspace/configFileTypes/tsConfigWithESM/apollo.config.ts +8 -0
  23. package/sampleWorkspace/configFileTypes/tsConfigWithESM/package.json +4 -0
  24. package/sampleWorkspace/configFileTypes/tsConfigWithESM/src/test.js +8 -0
  25. package/sampleWorkspace/localSchemaArray/src/test.js +1 -1
  26. package/sampleWorkspace/rover/apollo.config.yaml +2 -1
  27. package/sampleWorkspace/rover/src/test.graphql +11 -10
  28. package/sampleWorkspace/sampleWorkspace.code-workspace +3 -0
  29. package/src/__e2e__/runTests.js +1 -0
  30. package/src/language-server/__e2e__/clientSchema.e2e.ts +58 -28
  31. package/src/language-server/__e2e__/configFileTypes.e2e.ts +37 -0
  32. package/src/language-server/__e2e__/httpSchema.e2e.ts +23 -4
  33. package/src/language-server/__e2e__/localSchema.e2e.ts +23 -4
  34. package/src/language-server/__e2e__/localSchemaArray.e2e.ts +31 -6
  35. package/src/language-server/__e2e__/rover.e2e.ts +150 -0
  36. package/src/language-server/__e2e__/studioGraph.e2e.ts +18 -6
  37. package/src/language-server/__e2e__/utils.ts +116 -14
  38. package/src/language-server/config/cache-busting-resolver.js +4 -12
  39. package/src/language-server/config/cache-busting-resolver.types.ts +1 -1
  40. package/src/language-server/config/loadTsConfig.ts +33 -16
@@ -53,28 +53,12 @@ jobs:
53
53
  name: Test
54
54
  command: npm run test -- --runInBand
55
55
 
56
- E2E tests:
57
- executor: node
58
- steps:
59
- - checkout
60
- - npm-install
61
- - run: sudo apt update && sudo apt install -y libasound2 libgbm1 libgtk-3-0 libnss3 xvfb
62
- - run:
63
- name: Build
64
- command: npm run build:production
65
- - run:
66
- command: echo 'APOLLO_KEY="service:bob-123:489fhseo4"' > ./sampleWorkspace/spotifyGraph/.env
67
- - run:
68
- name: E2E tests
69
- command: xvfb-run -a npm run test:extension
70
-
71
56
  workflows:
72
57
  build-test-deploy:
73
58
  jobs:
74
59
  - lint
75
60
  - typescript
76
61
  - test
77
- - E2E tests
78
62
  security-scans:
79
63
  jobs:
80
64
  - secops/gitleaks:
@@ -0,0 +1,27 @@
1
+ name: Run E2E tests
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches:
6
+ - main
7
+ jobs:
8
+ test:
9
+ name: Run E2E tests
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - run: sudo apt update && sudo apt install -y libasound2 libgbm1 libgtk-3-0 libnss3 xvfb expect
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ cache: "npm"
17
+ - run: npm install
18
+ - run: npm run build:production
19
+ - run: echo 'APOLLO_KEY="service:bob-123:489fhseo4"' > ./sampleWorkspace/spotifyGraph/.env
20
+ - run: |
21
+ expect <<EOF
22
+ spawn ./node_modules/.bin/rover config auth --profile VSCode-E2E
23
+ expect "Copy the key and paste it into the prompt below."
24
+ send -- "test\n"
25
+ expect eof
26
+ EOF
27
+ - run: xvfb-run -a npm run test:extension
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ ## 2.3.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [`7a55e46b`](https://github.com/apollographql/vscode-graphql/commit/7a55e46bbce01af851a5daafd2507a7b353ea081) Thanks [@phryneas](https://github.com/phryneas)! - Fix styling in README.
14
+
3
15
  ## 2.3.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -33,9 +33,10 @@ Alternatively, you can create a `yaml`, `cjs`, `mjs`, or `ts` file with the same
33
33
 
34
34
  For the contents of this configuration file, select one of these options:
35
35
 
36
+ <h3>Configure extension for schemas published to Apollo GraphOS</h3>
36
37
  <details>
37
38
  <summary>
38
- <h3>Configure extension for schemas published to Apollo GraphOS</h3>
39
+ <i>Expand for instructions.</i>
39
40
  </summary>
40
41
 
41
42
  To get all the benefits of the VS Code experience, it's best to link the schema that is being developed against before installing the extension. The best way to do that is by [publishing a schema](https://www.apollographql.com/docs/graphos/delivery/publishing-schemas/) to the Apollo schema registry.
@@ -70,9 +71,10 @@ After this is done, VS Code can be reloaded and the Apollo integration will conn
70
71
 
71
72
  </details>
72
73
 
74
+ <h3 id="local-schemas">Configure extension to use introspection from a locally running service</h3>
73
75
  <details>
74
76
  <summary>
75
- <h3 id="local-schemas">Configure extension to use introspection from a locally running service</h3>
77
+ <i>Expand for instructions.</i>
76
78
  </summary>
77
79
 
78
80
  Sometimes it may make sense to link the editor to a locally running version of a schema to try out new designs that are in active development. To do this, the `apollo.config.json` file can be linked to a local service definition:
@@ -92,9 +94,10 @@ Linking to the local schema won't provide all features, such as switching graph
92
94
 
93
95
  </details>
94
96
 
97
+ <h3 id="local-schema-files">Configure extension for local schema files</h3>
95
98
  <details>
96
99
  <summary>
97
- <h3 id="local-schema-files">Configure extension for local schema files</h3>
100
+ <i>Expand for instructions.</i>
98
101
  </summary>
99
102
 
100
103
  You might not always have a running server to link to, so the extension also supports linking to a local schema file.
@@ -3,6 +3,8 @@ const path = require("path");
3
3
 
4
4
  module.exports = {
5
5
  moduleFileExtensions: ["js", "ts"],
6
+ // restrict the roots here so jest doesn't complain about *other* snapshots it sees as obsolete
7
+ roots: ["<rootDir>/src/language-server/__e2e__"],
6
8
  testMatch: ["<rootDir>/src/**/*.e2e.ts"],
7
9
  testEnvironment: "./src/__e2e__/vscode-environment.js",
8
10
  setupFiles: ["./src/__e2e__/setup.js"],
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.0",
5
+ "version": "2.3.2",
6
6
  "referenceID": "87197759-7617-40d0-b32e-46d378e907c7",
7
7
  "author": "Apollo GraphQL <opensource@apollographql.com>",
8
8
  "license": "MIT",
@@ -65,6 +65,7 @@
65
65
  "zod-validation-error": "3.3.1"
66
66
  },
67
67
  "devDependencies": {
68
+ "@apollo/rover": "0.27.0-alpha.0",
68
69
  "@changesets/changelog-github": "0.4.8",
69
70
  "@changesets/cli": "2.26.2",
70
71
  "@graphql-codegen/cli": "^5.0.2",
@@ -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
+ `;
@@ -2,7 +2,7 @@ import gql from "graphql-tag";
2
2
  gql`
3
3
  query Test {
4
4
  droid(id: "2000") {
5
- name
5
+ dName: name
6
6
  }
7
7
  planets {
8
8
  id
@@ -1,2 +1,3 @@
1
1
  rover:
2
- profile: default
2
+ profile: VSCode-E2E
3
+ bin: ../../node_modules/@apollo/rover/binary/rover-0.27.0-alpha.0
@@ -1,14 +1,15 @@
1
- """
2
- The query type, represents all of the entry points into our object graph
3
- """
1
+ extend schema
2
+ @link(
3
+ url: "https://specs.apollo.dev/federation/v2.8"
4
+ import: ["@key", "@override", "@requires", "@external", "@shareable"]
5
+ )
6
+
4
7
  type Query {
5
- me: User!
8
+ a: A
6
9
  }
7
10
 
8
- """
9
- Test
10
- """
11
- type User {
12
- id: ID!
13
- name: String!
11
+ type A @key(fields: "a") {
12
+ a: ID @override(from: "DNE")
13
+ b: String! @requires(fields: "c") @shareable
14
+ c: String! @external
14
15
  }
@@ -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
  }
@@ -18,6 +18,7 @@ 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";
21
22
  disposables.push(
22
23
  ...(await Promise.all([
23
24
  runMockServer(TEST_PORT, false, loadDefaultMocks),
@@ -1,24 +1,45 @@
1
1
  import { TextEditor } from "vscode";
2
- import { closeAllEditors, openEditor, testCompletion, getHover } from "./utils";
2
+ import {
3
+ closeAllEditors,
4
+ openEditor,
5
+ getCompletionItems,
6
+ getHover,
7
+ getPositionForEditor,
8
+ GetPositionFn,
9
+ } from "./utils";
3
10
 
4
11
  let editor: TextEditor;
12
+ let getPosition: GetPositionFn;
5
13
  beforeAll(async () => {
6
14
  closeAllEditors();
7
15
  editor = await openEditor("clientSchema/src/test.js");
16
+ getPosition = getPositionForEditor(editor);
8
17
  });
9
18
 
10
19
  test("completion", async () => {
11
- // dro|id
12
- await testCompletion(editor, [4, 7], [["droid", "Droid"]]);
13
- // na|me
14
- await testCompletion(editor, [5, 8], [["name", "String!"]]);
15
- // mo|del
16
- await testCompletion(editor, [6, 8], [["model", "String"]]);
20
+ expect(
21
+ (await getCompletionItems(editor, getPosition("dro|id")))[0],
22
+ ).toStrictEqual({
23
+ label: "droid",
24
+ detail: "Droid",
25
+ });
26
+ expect(
27
+ (await getCompletionItems(editor, getPosition("na|me")))[0],
28
+ ).toStrictEqual({
29
+ label: "name",
30
+ detail: "String!",
31
+ });
32
+ expect(
33
+ (await getCompletionItems(editor, getPosition("mo|del")))[0],
34
+ ).toStrictEqual({
35
+ label: "model",
36
+ detail: "String",
37
+ });
17
38
  });
18
39
 
19
40
  test("hover", async () => {
20
- // featu|reFlagDefer
21
- expect(await getHover(editor, [3, 10])).toMatchInlineSnapshot(`
41
+ expect(await getHover(editor, getPosition("featu|reFlagDefer")))
42
+ .toMatchInlineSnapshot(`
22
43
  "\`\`\`graphql
23
44
  Query.featureFlagDefer: Boolean!
24
45
  \`\`\`
@@ -32,8 +53,8 @@ Query.featureFlagDefer: Boolean!
32
53
  Whether to use defer"
33
54
  `);
34
55
 
35
- // @c|lient(always: false)
36
- expect(await getHover(editor, [3, 24])).toMatchInlineSnapshot(`
56
+ expect(await getHover(editor, getPosition("@c|lient(always: false)")))
57
+ .toMatchInlineSnapshot(`
37
58
  "\`\`\`graphql
38
59
  @client(always: Boolean)
39
60
  \`\`\`
@@ -43,8 +64,8 @@ Whether to use defer"
43
64
  Direct the client to resolve this field locally, either from the cache or local resolvers."
44
65
  `);
45
66
 
46
- // @client(alwa|ys: false)
47
- expect(await getHover(editor, [3, 33])).toMatchInlineSnapshot(`
67
+ expect(await getHover(editor, getPosition("@client(alwa|ys: false)")))
68
+ .toMatchInlineSnapshot(`
48
69
  "\`\`\`graphql
49
70
  always: Boolean
50
71
  \`\`\`
@@ -55,8 +76,8 @@ When true, the client will never use the cache for this value. See
55
76
  https://www.apollographql.com/docs/react/local-state/local-resolvers/#forcing-resolvers-with-clientalways-true"
56
77
  `);
57
78
 
58
- // @expo|rt(as: "defer")
59
- expect(await getHover(editor, [3, 49])).toMatchInlineSnapshot(`
79
+ expect(await getHover(editor, getPosition('@expo|rt(as: "defer")')))
80
+ .toMatchInlineSnapshot(`
60
81
  "\`\`\`graphql
61
82
  @export(as: String!)
62
83
  \`\`\`
@@ -66,7 +87,9 @@ https://www.apollographql.com/docs/react/local-state/local-resolvers/#forcing-re
66
87
  Export this locally resolved field as a variable to be used in the remainder of this query. See
67
88
  https://www.apollographql.com/docs/react/local-state/local-resolvers/#using-client-fields-as-variables"
68
89
  `);
69
- expect(await getHover(editor, [3, 53])).toMatchInlineSnapshot(`
90
+
91
+ expect(await getHover(editor, getPosition('@export(a|s: "defer")')))
92
+ .toMatchInlineSnapshot(`
70
93
  "\`\`\`graphql
71
94
  as: String!
72
95
  \`\`\`
@@ -76,8 +99,8 @@ as: String!
76
99
  The variable name to export this field as."
77
100
  `);
78
101
 
79
- // @nonre|active
80
- expect(await getHover(editor, [7, 28])).toMatchInlineSnapshot(`
102
+ expect(await getHover(editor, getPosition("@nonre|active")))
103
+ .toMatchInlineSnapshot(`
81
104
  "\`\`\`graphql
82
105
  @nonreactive
83
106
  \`\`\`
@@ -89,8 +112,9 @@ This allows parent components to fetch data to be rendered by their children wit
89
112
  https://www.apollographql.com/docs/react/data/directives#nonreactive"
90
113
  `);
91
114
 
92
- // @def|er(if: $defer, label: "fc")
93
- expect(await getHover(editor, [8, 14])).toMatchInlineSnapshot(`
115
+ expect(
116
+ await getHover(editor, getPosition('@def|er(if: $defer, label: "fc")')),
117
+ ).toMatchInlineSnapshot(`
94
118
  "\`\`\`graphql
95
119
  @defer(if: Boolean, label: String)
96
120
  \`\`\`
@@ -101,8 +125,10 @@ This directive enables your queries to receive data for specific fields incremen
101
125
  This is helpful whenever some fields in a query take much longer to resolve than others.
102
126
  https://www.apollographql.com/docs/react/data/directives#defer"
103
127
  `);
104
- //@defer(i|f: $defer, label: "fc")
105
- expect(await getHover(editor, [8, 18])).toMatchInlineSnapshot(`
128
+
129
+ expect(
130
+ await getHover(editor, getPosition('@defer(i|f: $defer, label: "fc")')),
131
+ ).toMatchInlineSnapshot(`
106
132
  "\`\`\`graphql
107
133
  if: Boolean
108
134
  \`\`\`
@@ -111,8 +137,10 @@ if: Boolean
111
137
 
112
138
  When true fragment may be deferred, if omitted defaults to true."
113
139
  `);
114
- //@defer(if: $defer, labe|l: "fc")
115
- expect(await getHover(editor, [8, 33])).toMatchInlineSnapshot(`
140
+
141
+ expect(
142
+ await getHover(editor, getPosition('@defer(if: $defer, labe|l: "fc")')),
143
+ ).toMatchInlineSnapshot(`
116
144
  "\`\`\`graphql
117
145
  label: String
118
146
  \`\`\`
@@ -123,8 +151,9 @@ A unique label across all @defer and @stream directives in an operation.
123
151
  This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragment.
124
152
  If provided, the GraphQL Server must add it to the payload."
125
153
  `);
126
- // @connec|tion(key: "feed")
127
- expect(await getHover(editor, [9, 53])).toMatchInlineSnapshot(`
154
+
155
+ expect(await getHover(editor, getPosition('@connec|tion(key: "feed")')))
156
+ .toMatchInlineSnapshot(`
128
157
  "\`\`\`graphql
129
158
  @connection(key: String!, filter: [String!])
130
159
  \`\`\`
@@ -134,8 +163,9 @@ If provided, the GraphQL Server must add it to the payload."
134
163
  Specify a custom store key for this result. See
135
164
  https://www.apollographql.com/docs/react/caching/advanced-topics/#the-connection-directive"
136
165
  `);
137
- // @connection(ke|y: "feed")
138
- expect(await getHover(editor, [9, 61])).toMatchInlineSnapshot(`
166
+
167
+ expect(await getHover(editor, getPosition('@connection(ke|y: "feed")')))
168
+ .toMatchInlineSnapshot(`
139
169
  "\`\`\`graphql
140
170
  key: String!
141
171
  \`\`\`
@@ -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
+ });
@@ -1,19 +1,38 @@
1
1
  import { TextEditor } from "vscode";
2
- import { closeAllEditors, openEditor, testCompletion, getHover } from "./utils";
2
+ import {
3
+ closeAllEditors,
4
+ openEditor,
5
+ getCompletionItems,
6
+ getHover,
7
+ getPositionForEditor,
8
+ GetPositionFn,
9
+ } from "./utils";
3
10
 
4
11
  let editor: TextEditor;
12
+ let getPosition: GetPositionFn;
5
13
  beforeAll(async () => {
6
14
  closeAllEditors();
7
15
  editor = await openEditor("httpSchema/src/test.js");
16
+ getPosition = getPositionForEditor(editor);
8
17
  });
9
18
 
10
19
  test("completion", async () => {
11
- await testCompletion(editor, [3, 7], [["books", "[Book]"]]);
12
- await testCompletion(editor, [5, 9], [["author", "String"]]);
20
+ expect(
21
+ (await getCompletionItems(editor, getPosition("bo|oks")))[0],
22
+ ).toStrictEqual({
23
+ label: "books",
24
+ detail: "[Book]",
25
+ });
26
+ expect(
27
+ (await getCompletionItems(editor, getPosition("au|thor")))[0],
28
+ ).toStrictEqual({
29
+ label: "author",
30
+ detail: "String",
31
+ });
13
32
  });
14
33
 
15
34
  test("hover", async () => {
16
- expect(await getHover(editor, [5, 9])).toMatchInlineSnapshot(`
35
+ expect(await getHover(editor, getPosition("au|thor"))).toMatchInlineSnapshot(`
17
36
  "\`\`\`graphql
18
37
  Book.author: String
19
38
  \`\`\`"
@@ -1,19 +1,38 @@
1
1
  import { TextEditor } from "vscode";
2
- import { closeAllEditors, openEditor, testCompletion, getHover } from "./utils";
2
+ import {
3
+ closeAllEditors,
4
+ openEditor,
5
+ getCompletionItems,
6
+ getHover,
7
+ GetPositionFn,
8
+ getPositionForEditor,
9
+ } from "./utils";
3
10
 
4
11
  let editor: TextEditor;
12
+ let getPosition: GetPositionFn;
5
13
  beforeAll(async () => {
6
14
  closeAllEditors();
7
15
  editor = await openEditor("localSchema/src/test.js");
16
+ getPosition = getPositionForEditor(editor);
8
17
  });
9
18
 
10
19
  test("completion", async () => {
11
- await testCompletion(editor, [3, 7], [["droid", "Droid"]]);
12
- await testCompletion(editor, [4, 8], [["name", "String!"]]);
20
+ expect(
21
+ (await getCompletionItems(editor, getPosition("dro|id")))[0],
22
+ ).toStrictEqual({
23
+ label: "droid",
24
+ detail: "Droid",
25
+ });
26
+ expect(
27
+ (await getCompletionItems(editor, getPosition("na|me")))[0],
28
+ ).toStrictEqual({
29
+ label: "name",
30
+ detail: "String!",
31
+ });
13
32
  });
14
33
 
15
34
  test("hover", async () => {
16
- expect(await getHover(editor, [4, 8])).toMatchInlineSnapshot(`
35
+ expect(await getHover(editor, getPosition("na|me"))).toMatchInlineSnapshot(`
17
36
  "\`\`\`graphql
18
37
  Droid.name: String!
19
38
  \`\`\`
@@ -1,20 +1,45 @@
1
1
  import { TextEditor } from "vscode";
2
- import { closeAllEditors, openEditor, testCompletion, getHover } from "./utils";
2
+ import {
3
+ closeAllEditors,
4
+ openEditor,
5
+ getCompletionItems,
6
+ getHover,
7
+ GetPositionFn,
8
+ getPositionForEditor,
9
+ } from "./utils";
3
10
 
4
11
  let editor: TextEditor;
12
+ let getPosition: GetPositionFn;
5
13
  beforeAll(async () => {
6
14
  closeAllEditors();
7
15
  editor = await openEditor("localSchemaArray/src/test.js");
16
+ getPosition = getPositionForEditor(editor);
8
17
  });
9
18
 
10
19
  test("completion", async () => {
11
- await testCompletion(editor, [3, 7], [["droid", "Droid"]]);
12
- await testCompletion(editor, [4, 8], [["name", "String!"]]);
13
- await testCompletion(editor, [6, 7], [["planets", "[Planet]"]]);
20
+ expect(
21
+ (await getCompletionItems(editor, getPosition("dro|id")))[0],
22
+ ).toStrictEqual({
23
+ label: "droid",
24
+ detail: "Droid",
25
+ });
26
+ expect(
27
+ (await getCompletionItems(editor, getPosition("d|Name: name")))[0],
28
+ ).toStrictEqual({
29
+ label: "name",
30
+ detail: "String!",
31
+ });
32
+ expect(
33
+ (await getCompletionItems(editor, getPosition("pl|anet")))[0],
34
+ ).toStrictEqual({
35
+ label: "planets",
36
+ detail: "[Planet]",
37
+ });
14
38
  });
15
39
 
16
40
  test("hover", async () => {
17
- expect(await getHover(editor, [4, 8])).toMatchInlineSnapshot(`
41
+ expect(await getHover(editor, getPosition("d|Name: name")))
42
+ .toMatchInlineSnapshot(`
18
43
  "\`\`\`graphql
19
44
  Droid.name: String!
20
45
  \`\`\`
@@ -23,7 +48,7 @@ Droid.name: String!
23
48
 
24
49
  What others call this droid"
25
50
  `);
26
- expect(await getHover(editor, [6, 7])).toMatchInlineSnapshot(`
51
+ expect(await getHover(editor, getPosition("pl|anet"))).toMatchInlineSnapshot(`
27
52
  "\`\`\`graphql
28
53
  Query.planets: [Planet]
29
54
  \`\`\`"
@@ -0,0 +1,150 @@
1
+ import { test as origTest } from "@jest/globals";
2
+ import { load } from "js-yaml";
3
+ import { readFileSync } from "node:fs";
4
+ import { execFileSync } from "node:child_process";
5
+ import { join } from "node:path";
6
+ import { ParsedApolloConfigFormat } from "../config";
7
+ import { TextEditor } from "vscode";
8
+ import {
9
+ closeAllEditors,
10
+ openEditor,
11
+ getCompletionItems,
12
+ getHover,
13
+ getPositionForEditor,
14
+ GetPositionFn,
15
+ getFullSemanticTokens,
16
+ getDefinitions,
17
+ } from "./utils";
18
+
19
+ // we want to skip these tests unless the user running them has a rover config profile named "VSCode-E2E"
20
+ let test = origTest.skip;
21
+ try {
22
+ const roverProjectDir = join(__dirname, "../../../sampleWorkspace/rover");
23
+ const config = load(
24
+ readFileSync(join(roverProjectDir, "apollo.config.yaml"), "utf-8"),
25
+ ) as ParsedApolloConfigFormat;
26
+ const roverBin = join(roverProjectDir, config.rover!.bin);
27
+ const result = execFileSync(roverBin, [
28
+ "config",
29
+ "list",
30
+ "--format=json",
31
+ ]).toString("utf8");
32
+ const parsed = JSON.parse(result);
33
+ if (parsed.data.profiles.includes("VSCode-E2E")) {
34
+ test = origTest;
35
+ }
36
+ } catch (e) {}
37
+ if (test === origTest.skip) {
38
+ console.info(
39
+ "Skipping rover E2E tests: no profile with the name 'VSCode-E2E'\n" +
40
+ "You can create one by running `rover config auth --profile VSCode-E2E`",
41
+ );
42
+ }
43
+
44
+ let editor: TextEditor;
45
+ let getPosition: GetPositionFn;
46
+ beforeAll(async () => {
47
+ closeAllEditors();
48
+ editor = await openEditor("rover/src/test.graphql");
49
+ getPosition = getPositionForEditor(editor);
50
+ });
51
+
52
+ test("hover", async () => {
53
+ expect(await getHover(editor, getPosition("@over|ride(from")))
54
+ .toMatchInlineSnapshot(`
55
+ "The [\`@override\`](https://www.apollographql.com/docs/federation/federated-schemas/federated-directives/#override) directive indicates that an object field is now resolved by this subgraph instead of another subgraph where it's also defined. This enables you to migrate a field from one subgraph to another.
56
+
57
+ You can apply \`@override\` to entity fields and fields of the root operation types (such as \`Query\` and \`Mutation\`). A second \`label\` argument can be used to progressively override a field. See [the docs](https://www.apollographql.com/docs/federation/entities/migrate-fields/#incremental-migration-with-progressive-override) for more information.
58
+ ***
59
+ \`\`\`graphql
60
+ directive @override(from: String!, label: String) on FIELD_DEFINITION
61
+ \`\`\`"
62
+ `);
63
+ });
64
+
65
+ test("completion", async () => {
66
+ expect(await getCompletionItems(editor, getPosition("@over|ride(from")))
67
+ .toMatchInlineSnapshot(`
68
+ [
69
+ {
70
+ "detail": undefined,
71
+ "label": "@deprecated",
72
+ },
73
+ {
74
+ "detail": undefined,
75
+ "label": "@external",
76
+ },
77
+ {
78
+ "detail": undefined,
79
+ "label": "@federation__authenticated",
80
+ },
81
+ {
82
+ "detail": undefined,
83
+ "label": "@federation__inaccessible",
84
+ },
85
+ {
86
+ "detail": undefined,
87
+ "label": "@federation__policy(…)",
88
+ },
89
+ {
90
+ "detail": undefined,
91
+ "label": "@federation__provides(…)",
92
+ },
93
+ {
94
+ "detail": undefined,
95
+ "label": "@federation__requiresScopes(…)",
96
+ },
97
+ {
98
+ "detail": undefined,
99
+ "label": "@federation__tag(…)",
100
+ },
101
+ {
102
+ "detail": undefined,
103
+ "label": "@override(…)",
104
+ },
105
+ {
106
+ "detail": undefined,
107
+ "label": "@requires(…)",
108
+ },
109
+ {
110
+ "detail": undefined,
111
+ "label": "@shareable",
112
+ },
113
+ ]
114
+ `);
115
+ });
116
+
117
+ test("semantic tokens", async () => {
118
+ const tokens = await getFullSemanticTokens(editor);
119
+ expect(tokens[0]).toStrictEqual({
120
+ startPosition: getPosition('fields: "|a"'),
121
+ endPosition: getPosition('fields: "a|"'),
122
+ tokenType: "property",
123
+ tokenModifiers: [],
124
+ });
125
+ expect(tokens[1]).toStrictEqual({
126
+ startPosition: getPosition('fields: "|c"'),
127
+ endPosition: getPosition('fields: "c|"'),
128
+ tokenType: "property",
129
+ tokenModifiers: [],
130
+ });
131
+ });
132
+
133
+ test("definitions", async () => {
134
+ const definitions = await getDefinitions(editor, getPosition("a: |A"));
135
+
136
+ expect(definitions[0].targetUri.toString()).toBe(
137
+ editor.document.uri.toString(),
138
+ );
139
+ expect(
140
+ editor.document.getText(definitions[0].targetSelectionRange!),
141
+ ).toMatchInlineSnapshot(`"A"`);
142
+ expect(editor.document.getText(definitions[0].targetRange))
143
+ .toMatchInlineSnapshot(`
144
+ "type A @key(fields: "a") {
145
+ a: ID @override(from: "DNE")
146
+ b: String! @requires(fields: "c") @shareable
147
+ c: String! @external
148
+ }"
149
+ `);
150
+ });
@@ -2,15 +2,14 @@ import { TextEditor } from "vscode";
2
2
  import {
3
3
  closeAllEditors,
4
4
  openEditor,
5
- testCompletion,
5
+ getCompletionItems,
6
6
  getHover,
7
7
  getExtension,
8
8
  getOutputChannelDocument,
9
9
  reloadService,
10
+ getPositionForEditor,
10
11
  } from "./utils";
11
12
  import mocks from "../../__e2e__/mocks.js";
12
- import vscode from "vscode";
13
- import { scheduler } from "node:timers/promises";
14
13
 
15
14
  const mockPort = Number(process.env.MOCK_SERVER_PORT);
16
15
  beforeAll(async () => {
@@ -19,13 +18,26 @@ beforeAll(async () => {
19
18
 
20
19
  test("completion", async () => {
21
20
  const editor = await openEditor("spotifyGraph/src/test.js");
22
- await testCompletion(editor, [4, 9], [["profile", "CurrentUserProfile!"]]);
23
- await testCompletion(editor, [6, 15], [["displayName", "String"]]);
21
+ const getPosition = getPositionForEditor(editor);
22
+ expect(
23
+ (await getCompletionItems(editor, getPosition("pr|ofile")))[0],
24
+ ).toStrictEqual({
25
+ label: "profile",
26
+ detail: "CurrentUserProfile!",
27
+ });
28
+ expect(
29
+ (await getCompletionItems(editor, getPosition("dis|playName")))[0],
30
+ ).toStrictEqual({
31
+ label: "displayName",
32
+ detail: "String",
33
+ });
24
34
  });
25
35
 
26
36
  test("hover", async () => {
27
37
  const editor = await openEditor("spotifyGraph/src/test.js");
28
- expect(await getHover(editor, [4, 9])).toMatchInlineSnapshot(`
38
+ const getPosition = getPositionForEditor(editor);
39
+ expect(await getHover(editor, getPosition("pr|ofile")))
40
+ .toMatchInlineSnapshot(`
29
41
  "\`\`\`graphql
30
42
  CurrentUser.profile: CurrentUserProfile!
31
43
  \`\`\`
@@ -7,6 +7,26 @@ 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 };
11
+
12
+ export type GetPositionFn = ReturnType<typeof getPositionForEditor>;
13
+ export function getPositionForEditor(editor: vscode.TextEditor) {
14
+ return function getPosition(cursor: `${string}|${string}`) {
15
+ if (cursor.indexOf("|") !== cursor.lastIndexOf("|")) {
16
+ throw new Error(
17
+ "`getPosition` cursor description can only contain one |",
18
+ );
19
+ }
20
+ const text = editor.document.getText();
21
+ const idx = text.indexOf(cursor.replace("|", ""));
22
+ if (idx !== text.lastIndexOf(cursor.replace("|", ""))) {
23
+ throw new Error("`getPosition` cursor description is not unique");
24
+ }
25
+ const cursorIndex = idx + cursor.indexOf("|");
26
+ const position = editor.document.positionAt(cursorIndex);
27
+ return position;
28
+ };
29
+ }
10
30
 
11
31
  export async function closeAllEditors() {
12
32
  while (vscode.window.visibleTextEditors.length > 0) {
@@ -46,27 +66,32 @@ export function waitForLSP(file: string) {
46
66
  uri.toString(),
47
67
  );
48
68
  expect(stats.loaded).toBe(true);
49
- return stats;
69
+ return stats as ProjectStats & { loaded: true };
50
70
  });
51
71
  }
52
72
 
53
- export async function testCompletion(
73
+ export async function getCompletionItems(
54
74
  editor: vscode.TextEditor,
55
- [line, character]: [number, number],
56
- expected: Array<[label: string, detail: string]>,
75
+ position: vscode.Position,
57
76
  ) {
77
+ let result: { label: string; detail: string | undefined }[] | undefined = [];
58
78
  await waitFor(async () => {
59
- editor.selection = new vscode.Selection(line, character, line, character);
79
+ editor.selection = new vscode.Selection(
80
+ position.line,
81
+ position.character,
82
+ position.line,
83
+ position.character,
84
+ );
60
85
  // without this, the completion list is not updated
61
86
  await scheduler.wait(300);
62
87
  const completions =
63
88
  await vscode.commands.executeCommand<vscode.CompletionList>(
64
89
  "vscode.executeCompletionItemProvider",
65
90
  editor.document.uri,
66
- new vscode.Position(line, character),
91
+ position,
67
92
  );
68
-
69
- const labels = completions.items.slice(0, expected.length).map((item) =>
93
+ expect(completions.items).not.toHaveLength(0);
94
+ const labels = completions.items.map((item) =>
70
95
  typeof item.label === "string"
71
96
  ? { label: item.label, detail: "" }
72
97
  : {
@@ -74,23 +99,27 @@ export async function testCompletion(
74
99
  detail: item.detail,
75
100
  },
76
101
  );
77
- expect(labels).toStrictEqual(
78
- expected.map(([label, detail]) => ({ label, detail })),
79
- );
102
+ result = labels;
80
103
  });
104
+ return result;
81
105
  }
82
106
 
83
107
  export async function getHover(
84
108
  editor: vscode.TextEditor,
85
- [line, character]: [number, number],
109
+ position: vscode.Position,
86
110
  ) {
87
- editor.selection = new vscode.Selection(line, character, line, character);
111
+ editor.selection = new vscode.Selection(
112
+ position.line,
113
+ position.character,
114
+ position.line,
115
+ position.character,
116
+ );
88
117
  // without this, the completion list is not updated
89
118
  await scheduler.wait(300);
90
119
  const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
91
120
  "vscode.executeHoverProvider",
92
121
  editor.document.uri,
93
- new vscode.Position(line, character),
122
+ position,
94
123
  );
95
124
 
96
125
  const item = hovers[0];
@@ -149,3 +178,76 @@ export async function reloadService() {
149
178
  await reloaded;
150
179
  await scheduler.wait(100);
151
180
  }
181
+
182
+ export async function getFullSemanticTokens(editor: vscode.TextEditor) {
183
+ const legend = await vscode.commands.executeCommand<
184
+ vscode.SemanticTokensLegend | undefined
185
+ >(
186
+ // https://github.com/microsoft/vscode/blob/d90ab31527203cdb15056df0dc84ab9ddcbbde40/src/vs/workbench/api/common/extHostApiCommands.ts#L220
187
+ "vscode.provideDocumentSemanticTokensLegend",
188
+ editor.document.uri,
189
+ );
190
+ expect(legend).toBeDefined();
191
+ const tokens = await vscode.commands.executeCommand<
192
+ vscode.SemanticTokens | undefined
193
+ >(
194
+ // https://github.com/microsoft/vscode/blob/d90ab31527203cdb15056df0dc84ab9ddcbbde40/src/vs/workbench/api/common/extHostApiCommands.ts#L229
195
+ "vscode.provideDocumentSemanticTokens",
196
+ editor.document.uri,
197
+ );
198
+ expect(tokens).toBeDefined();
199
+
200
+ return decodeSemanticTokens(tokens!, legend!);
201
+ }
202
+
203
+ function decodeSemanticTokens(
204
+ tokens: vscode.SemanticTokens,
205
+ legend: vscode.SemanticTokensLegend,
206
+ ) {
207
+ const tokenArr = Array.from(tokens.data);
208
+ const decodedTokens: {
209
+ startPosition: vscode.Position;
210
+ endPosition: vscode.Position;
211
+ tokenType: string;
212
+ tokenModifiers: string[];
213
+ }[] = [];
214
+ let line = 0,
215
+ start = 0;
216
+ for (let pos = 0; pos < tokenArr.length; pos += 5) {
217
+ const [deltaLine, deltaStart, length, tokenType, tokenModifiers] =
218
+ tokenArr.slice(pos, pos + 5);
219
+ if (deltaLine) {
220
+ line += deltaLine;
221
+ start = 0;
222
+ }
223
+ start += deltaStart;
224
+ const decodedModifiers: string[] = [];
225
+ for (let modifiers = tokenModifiers; modifiers > 0; modifiers >>= 1) {
226
+ decodedModifiers.push(legend.tokenModifiers[modifiers & 0xf]);
227
+ }
228
+ const startPosition = new vscode.Position(line, start);
229
+ const endPosition = startPosition.translate(0, length);
230
+ decodedTokens.push({
231
+ startPosition,
232
+ endPosition,
233
+ tokenType: legend.tokenTypes[tokenType],
234
+ tokenModifiers: decodedModifiers,
235
+ });
236
+ }
237
+ return decodedTokens;
238
+ }
239
+
240
+ export async function getDefinitions(
241
+ editor: vscode.TextEditor,
242
+ position: vscode.Position,
243
+ ) {
244
+ return vscode.commands.executeCommand<
245
+ // this is not the correct type, but the best match with public types I could find
246
+ vscode.LocationLink[]
247
+ >(
248
+ // https://github.com/microsoft/vscode/blob/d90ab31527203cdb15056df0dc84ab9ddcbbde40/src/vs/workbench/api/common/extHostApiCommands.ts#L87
249
+ "vscode.executeDefinitionProvider",
250
+ editor.document.uri,
251
+ position,
252
+ );
253
+ }
@@ -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
 
@@ -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
+ }