├── .changeset
├── README.md
└── config.json
├── .circleci
└── config.yml
├── .eslintrc.js
├── .git-blame-ignore-revs
├── .gitattributes
├── .github
└── workflows
│ ├── E2E.yml
│ ├── build-prs.yml
│ └── release.yml
├── .gitignore
├── .gitleaks.toml
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── CODEOWNERS
├── LICENSE
├── README.md
├── codegen.yml
├── graphql.configuration.json
├── images
├── IconRun.svg
├── apollo.svg
├── engine-stats.png
├── icon-apollo-blue-400x400.png
├── marketplace
│ ├── apollo-wordmark.png
│ ├── autocomplete.gif
│ ├── federation-directive-hover.png
│ ├── jump-to-def.gif
│ ├── perf-annotation.png
│ ├── stats.gif
│ ├── type-info.png
│ └── warnings-and-errors.gif
├── query-with-vars.gif
└── variable-argument-completion.gif
├── jest.config.ts
├── jest.e2e.config.js
├── package-lock.json
├── package.json
├── renovate.json
├── sampleWorkspace
├── clientSchema
│ ├── apollo.config.cjs
│ ├── src
│ │ ├── clientSchema.js
│ │ └── test.js
│ └── starwarsSchema.graphql
├── configFileTypes
│ ├── cjsConfig
│ │ ├── apollo.config.cjs
│ │ ├── package.json
│ │ ├── src
│ │ │ └── test.js
│ │ └── starwarsSchema.graphql
│ ├── jsConfigWithCJS
│ │ ├── apollo.config.js
│ │ ├── package.json
│ │ ├── src
│ │ │ └── test.js
│ │ └── starwarsSchema.graphql
│ ├── jsConfigWithESM
│ │ ├── apollo.config.js
│ │ ├── package.json
│ │ ├── src
│ │ │ └── test.js
│ │ └── starwarsSchema.graphql
│ ├── mjsConfig
│ │ ├── apollo.config.mjs
│ │ ├── package.json
│ │ ├── src
│ │ │ └── test.js
│ │ └── starwarsSchema.graphql
│ ├── tsConfigWithCJS
│ │ ├── apollo.config.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ └── test.js
│ │ └── starwarsSchema.graphql
│ └── tsConfigWithESM
│ │ ├── apollo.config.ts
│ │ ├── package.json
│ │ ├── src
│ │ └── test.js
│ │ └── starwarsSchema.graphql
├── fixtures
│ └── starwarsSchema.graphql
├── httpSchema
│ ├── apollo.config.ts
│ ├── self-signed.crt
│ ├── self-signed.key
│ └── src
│ │ └── test.js
├── localSchema
│ ├── apollo.config.ts
│ ├── src
│ │ └── test.js
│ └── starwarsSchema.graphql
├── localSchemaArray
│ ├── apollo.config.json
│ ├── planets.graphql
│ ├── src
│ │ └── test.js
│ └── starwarsSchema.graphql
├── rover
│ ├── apollo.config.yaml
│ ├── src
│ │ ├── test.graphql
│ │ └── test.js
│ └── supergraph.yaml
├── sampleWorkspace.code-workspace
└── spotifyGraph
│ ├── apollo.config.mjs
│ └── src
│ └── test.js
├── schemas
├── apollo.config.schema.json
└── supergraph_config_schema.json
├── src
├── __e2e__
│ ├── mockServer.js
│ ├── mocks.js
│ ├── run.js
│ ├── runTests.js
│ ├── setup.js
│ ├── vscode-environment.js
│ └── vscode.js
├── __mocks__
│ └── fs.js
├── __tests__
│ └── statusBar.test.ts
├── build.js
├── debug.ts
├── devtools
│ ├── DevToolsViewProvider.ts
│ └── server.ts
├── env
│ ├── index.ts
│ └── typescript-utility-types.ts
├── extension.ts
├── language-server
│ ├── __e2e__
│ │ ├── clientSchema.e2e.ts
│ │ ├── configFileTypes.e2e.ts
│ │ ├── httpSchema.e2e.ts
│ │ ├── localSchema.e2e.ts
│ │ ├── localSchemaArray.e2e.ts
│ │ ├── rover.e2e.ts
│ │ ├── studioGraph.e2e.ts
│ │ └── utils.ts
│ ├── __tests__
│ │ ├── diagnostics.test.ts
│ │ ├── document.test.ts
│ │ ├── fileSet.test.ts
│ │ └── fixtures
│ │ │ ├── TypeScript.tmLanguage.json
│ │ │ ├── documents
│ │ │ ├── commentWithTemplate.ts
│ │ │ ├── commentWithTemplate.ts.snap
│ │ │ ├── functionCall.ts
│ │ │ ├── functionCall.ts.snap
│ │ │ ├── taggedTemplate.ts
│ │ │ ├── taggedTemplate.ts.snap
│ │ │ ├── templateWithComment.ts
│ │ │ └── templateWithComment.ts.snap
│ │ │ └── starwarsSchema.ts
│ ├── config
│ │ ├── __tests__
│ │ │ ├── config.ts
│ │ │ ├── loadConfig.ts
│ │ │ └── utils.ts
│ │ ├── cache-busting-resolver.js
│ │ ├── cache-busting-resolver.types.ts
│ │ ├── config.ts
│ │ ├── index.ts
│ │ ├── loadConfig.ts
│ │ ├── loadTsConfig.ts
│ │ ├── utils.ts
│ │ └── which.d.ts
│ ├── diagnostics.ts
│ ├── document.ts
│ ├── engine
│ │ ├── index.ts
│ │ └── operations
│ │ │ ├── frontendUrlRoot.ts
│ │ │ └── schemaTagsAndFieldStats.ts
│ ├── errors
│ │ ├── __tests__
│ │ │ └── NoMissingClientDirectives.test.ts
│ │ ├── logger.ts
│ │ └── validation.ts
│ ├── fileSet.ts
│ ├── format.ts
│ ├── graphqlTypes.ts
│ ├── index.ts
│ ├── loadingHandler.ts
│ ├── project
│ │ ├── base.ts
│ │ ├── client.ts
│ │ ├── defaultClientSchema.ts
│ │ ├── internal.ts
│ │ └── rover
│ │ │ ├── DocumentSynchronization.ts
│ │ │ ├── __tests__
│ │ │ └── DocumentSynchronization.test.ts
│ │ │ └── project.ts
│ ├── providers
│ │ └── schema
│ │ │ ├── __tests__
│ │ │ └── file.ts
│ │ │ ├── base.ts
│ │ │ ├── endpoint.ts
│ │ │ ├── engine.ts
│ │ │ ├── file.ts
│ │ │ └── index.ts
│ ├── server.ts
│ ├── typings
│ │ └── graphql.d.ts
│ ├── utilities
│ │ ├── __tests__
│ │ │ ├── graphql.test.ts
│ │ │ ├── source.test.ts
│ │ │ └── uri.ts
│ │ ├── debouncer.ts
│ │ ├── debug.ts
│ │ ├── graphql.ts
│ │ ├── index.ts
│ │ ├── languageIdForExtension.ts
│ │ ├── source.ts
│ │ └── uri.ts
│ └── workspace.ts
├── languageServerClient.ts
├── messages.ts
├── statusBar.ts
├── tools
│ ├── __tests__
│ │ ├── buildServiceDefinition.test.ts
│ │ └── snapshotSerializers
│ │ │ ├── astSerializer.ts
│ │ │ └── graphQLTypeSerializer.ts
│ ├── buildServiceDefinition.ts
│ ├── index.ts
│ ├── schema
│ │ ├── index.ts
│ │ ├── resolveObject.ts
│ │ └── resolverMap.ts
│ └── utilities
│ │ ├── getLanguageInformation.ts
│ │ ├── graphql.ts
│ │ ├── index.ts
│ │ ├── invariant.ts
│ │ ├── languageInformation.ts
│ │ └── predicates.ts
└── utils.ts
├── start-ac.mjs
├── syntaxes
├── graphql.dart.json
├── graphql.ex.json
├── graphql.js.json
├── graphql.json
├── graphql.lua.json
├── graphql.py.json
├── graphql.rb.json
└── graphql.re.json
├── tsconfig.build.json
└── tsconfig.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | { "repo": "apollographql/vscode-graphql" }
6 | ],
7 | "commit": false,
8 | "fixed": [],
9 | "linked": [],
10 | "access": "public",
11 | "baseBranch": "main",
12 | "updateInternalDependencies": "patch",
13 | "ignore": []
14 | }
15 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | secops: apollo/circleci-secops-orb@2.0.7
5 |
6 | executors:
7 | node:
8 | docker:
9 | - image: cimg/node:22.12.0
10 | working_directory: ~/vscode-graphql
11 |
12 | commands:
13 | npm-install:
14 | steps:
15 | - restore_cache:
16 | name: Restore npm cache
17 | keys:
18 | - npm-packages-{{ checksum "package-lock.json" }}--{{ checksum ".circleci/config.yml" }}
19 | - run:
20 | name: Install dependencies
21 | command: npm ci --prefer-offline
22 | - save_cache:
23 | name: Save npm cache
24 | key: npm-packages-{{ checksum "package-lock.json" }}--{{ checksum ".circleci/config.yml" }}
25 | paths:
26 | - ~/.npm
27 |
28 | jobs:
29 | lint:
30 | executor: node
31 | steps:
32 | - checkout
33 | - npm-install
34 | - run:
35 | name: Run lint (currenty prettier)
36 | command: npm run lint
37 |
38 | typescript:
39 | executor: node
40 | steps:
41 | - checkout
42 | - npm-install
43 | - run:
44 | name: TypeScript Check
45 | command: npm run typecheck
46 |
47 | test:
48 | executor: node
49 | steps:
50 | - checkout
51 | - npm-install
52 | - run:
53 | name: Test
54 | command: npm run test -- --runInBand
55 |
56 | workflows:
57 | build-test-deploy:
58 | jobs:
59 | - lint
60 | - typescript
61 | - test
62 | security-scans:
63 | jobs:
64 | - secops/gitleaks:
65 | context:
66 | - platform-docker-ro
67 | - github-orb
68 | - secops-oidc
69 | git-base-revision: <<#pipeline.git.base_revision>><><>
70 | git-revision: << pipeline.git.revision >>
71 | - secops/semgrep:
72 | context:
73 | - secops-oidc
74 | - github-orb
75 | git-base-revision: <<#pipeline.git.base_revision>><><>
76 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | plugins: ["prettier", "@typescript-eslint"],
4 | // Skip generated file.
5 | ignorePatterns: ["src/language-server/graphqlTypes.ts"],
6 | rules: {
7 | "prettier/prettier": "error",
8 | },
9 | extends: ["prettier"],
10 | };
11 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Run format on codebase after upgrading Prettier
2 | d47effa7e7ad3494349d0ed5957501d1372ce59e
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | package-lock.json -diff
2 |
--------------------------------------------------------------------------------
/.github/workflows/E2E.yml:
--------------------------------------------------------------------------------
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: "${{ matrix.os }}"
11 | strategy:
12 | matrix:
13 | version: ["1.90.0", "stable", "insiders"]
14 | os: [ubuntu-latest]
15 | include:
16 | - version: "stable"
17 | os: "windows-latest"
18 | steps:
19 | - run: sudo apt update && sudo apt install -y libasound2t64 libgbm1 libgtk-3-0 libnss3 xvfb expect
20 | if: runner.os == 'Linux'
21 | - uses: actions/checkout@v4
22 | - uses: actions/setup-node@v4
23 | with:
24 | cache: "npm"
25 | - run: npm install
26 | - run: echo 'APOLLO_KEY="service:bob-123:489fhseo4"' > ./sampleWorkspace/spotifyGraph/.env
27 | shell: bash
28 |
29 | # Print rover version per OS
30 | - name: Install & Configure Rover (Linux)
31 | run: ./node_modules/.bin/rover --version
32 | if: runner.os == 'Linux'
33 | - name: Install Rover (Windows)
34 | run: ./node_modules/.bin/rover.cmd --version
35 | if: runner.os == 'Windows'
36 |
37 | # auth rover per OS
38 | - name: Configure Rover (Linux)
39 | run: |
40 | expect < jest.config.ts
73 | shell: bash
74 | if: runner.os == 'Windows'
75 |
76 | - run: npm run build:production
77 |
78 | # Run test per OS
79 | - name: "Run Extension E2E tests (Linux)"
80 | run: xvfb-run -a npm run test:extension
81 | env:
82 | VSCODE_VERSION: "${{ matrix.version }}"
83 | if: runner.os == 'Linux'
84 | - name: "Run Extension E2E tests (Windows)"
85 | run: npm run test:extension
86 | env:
87 | VSCODE_VERSION: "${{ matrix.version }}"
88 | if: runner.os == 'Windows'
89 |
--------------------------------------------------------------------------------
/.github/workflows/build-prs.yml:
--------------------------------------------------------------------------------
1 | name: Bundle Extension as Artifact Download
2 | on:
3 | pull_request:
4 | jobs:
5 | test:
6 | name: Bundle Extension as Artifact Download
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-node@v4
11 | with:
12 | cache: "npm"
13 | - run: npm install
14 | - run: echo PKG_VERSION="$(git show --no-patch --format=0.0.0-build-%ct.pr-${{ github.event.pull_request.number }}.commit-%h)" >> $GITHUB_ENV
15 | - run: npm pkg set "version=${{ env.PKG_VERSION }}"
16 | - run: npx -y @vscode/vsce package --out vscode-apollo-${{ env.PKG_VERSION }}.vsix
17 |
18 | - uses: actions/upload-artifact@v4
19 | id: artifact-upload-step
20 | with:
21 | name: vscode-apollo-${{ env.PKG_VERSION }}
22 | path: vscode-apollo-${{ env.PKG_VERSION }}.vsix
23 | retention-days: 14
24 |
25 | - name: Output artifact URL
26 | run: echo 'Artifact URL is ${{ steps.artifact-upload-step.outputs.artifact-url }}'
27 |
28 | - name: Find Comment
29 | uses: peter-evans/find-comment@v3
30 | id: fc
31 | with:
32 | issue-number: ${{ github.event.pull_request.number }}
33 | comment-author: "github-actions[bot]"
34 | body-includes:
35 |
36 | - name: Create comment
37 | uses: peter-evans/create-or-update-comment@v4
38 | with:
39 | issue-number: ${{ github.event.pull_request.number }}
40 | comment-id: ${{ steps.fc.outputs.comment-id }}
41 | edit-mode: replace
42 | body: |
43 |
44 | You can download the latest build of the extension for this PR here:
45 | [vscode-apollo-${{ env.PKG_VERSION }}.zip](${{ steps.artifact-upload-step.outputs.artifact-url }}).
46 |
47 | To install the extension, download the file, unzip it and install it in VS Code by selecting "Install from VSIX..." in the Extensions view.
48 |
49 | Alternatively, run
50 | ```sh
51 | code --install-extension vscode-apollo-${{ env.PKG_VERSION }}.vsix --force
52 | ```
53 | from the command line.
54 |
55 | For older builds, please see the edit history of this comment.
56 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Changesets Release
13 | # Prevents action from creating a PR on forks
14 | if: github.repository == 'apollographql/vscode-graphql'
15 | runs-on: ubuntu-latest
16 | # Permissions necessary for Changesets to push a new branch and open PRs
17 | # (for automated Version Packages PRs), and request the JWT for provenance.
18 | # More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
19 | permissions:
20 | contents: write
21 | pull-requests: write
22 | id-token: write
23 | steps:
24 | - name: Checkout repo
25 | uses: actions/checkout@v4
26 | with:
27 | # Fetch entire git history so Changesets can generate changelogs
28 | # with the correct commits
29 | fetch-depth: 0
30 |
31 | - name: Check for pre.json file existence
32 | id: check_files
33 | uses: andstor/file-existence-action@v2.0.0
34 | with:
35 | files: ".changeset/pre.json"
36 |
37 | - name: Append NPM token to .npmrc
38 | run: |
39 | cat << EOF > "$HOME/.npmrc"
40 | //registry.npmjs.org/:_authToken=$NPM_TOKEN
41 | EOF
42 | env:
43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
44 |
45 | - name: Setup Node.js 18.x
46 | uses: actions/setup-node@v3
47 | with:
48 | node-version: 18.x
49 |
50 | - name: Install dependencies
51 | run: npm ci
52 |
53 | - name: Create release PR or publish to npm + GitHub
54 | id: changesets
55 | if: steps.check_files.outputs.files_exists == 'false'
56 | uses: changesets/action@v1
57 | with:
58 | version: npm run changeset-version
59 | publish: npm run changeset-publish
60 | env:
61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
63 |
64 | - name: Attach VSX to GitHub release
65 | if: steps.changesets.outcome == 'success' && steps.changesets.outputs.published == 'true'
66 | run: |
67 | npx -y @vscode/vsce package --out "vscode-apollo-$VERSION.vsix"
68 | gh release upload "v$VERSION" "vscode-apollo-$VERSION.vsix"
69 | env:
70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71 | VERSION: ${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}
72 |
73 | - name: Publish to Open VSX Registry
74 | if: steps.changesets.outcome == 'success' && steps.changesets.outputs.published == 'true'
75 | uses: HaaLeo/publish-vscode-extension@v1
76 | with:
77 | pat: ${{ secrets.OPEN_VSX_TOKEN }}
78 | baseContentUrl: https://raw.githubusercontent.com/apollographql/vscode-graphql
79 |
80 | - name: Publish to Visual Studio Marketplace
81 | if: steps.changesets.outcome == 'success' && steps.changesets.outputs.published == 'true'
82 | uses: HaaLeo/publish-vscode-extension@v1
83 | with:
84 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }}
85 | registryUrl: https://marketplace.visualstudio.com
86 | baseContentUrl: https://raw.githubusercontent.com/apollographql/vscode-graphql
87 |
88 | - name: Send a Slack notification on publish
89 | if: steps.changesets.outcome == 'success' && steps.changesets.outputs.published == 'true'
90 | id: slack
91 | uses: slackapi/slack-github-action@v1.24.0
92 | with:
93 | # Slack channel id, channel name, or user id to post message
94 | # See also: https://api.slack.com/methods/chat.postMessage#channels
95 | # You can pass in multiple channels to post to by providing
96 | # a comma-delimited list of channel IDs
97 | channel-id: "C02J316U84V"
98 | payload: |
99 | {
100 | "blocks": [
101 | {
102 | "type": "section",
103 | "text": {
104 | "type": "mrkdwn",
105 | "text": "A new version of `vscode-apollo` was released: :rocket:"
106 | }
107 | }
108 | ]
109 | }
110 | env:
111 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
112 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 |
4 | # TypeScript incremental compilation cache
5 | *.tsbuildinfo
6 |
7 | # Visual Studio Code workspace settings
8 | .vscode/*
9 | !.vscode/settings.json
10 | !.vscode/tasks.json
11 | !.vscode/launch.json
12 | !.vscode/extensions.json
13 |
14 | .DS_Store
15 | vscode-apollo-*.vsix
16 | .env
17 |
18 | # files generated from tests
19 | __tmp__*
20 | .vscode-test
21 | .yalc
22 | yalc.lock
23 |
--------------------------------------------------------------------------------
/.gitleaks.toml:
--------------------------------------------------------------------------------
1 | # This file exists primarily to influence scheduled scans that Apollo runs of all repos in Apollo-managed orgs.
2 | # This is an Apollo-Internal link, but more information about these scans is available here:
3 | # https://apollographql.atlassian.net/wiki/spaces/SecOps/pages/81330213/Everything+Static+Application+Security+Testing#Scheduled-Scans.1
4 | #
5 | # Apollo is using Gitleaks (https://github.com/gitleaks/gitleaks) to run these scans.
6 | # However, this file is not something that Gitleaks natively consumes. This file is an
7 | # Apollo-convention. Prior to scanning a repo, Apollo merges
8 | # our standard Gitleaks configuration (which is largely just the Gitleaks-default config) with
9 | # this file if it exists in a repo. The combined config is then used to scan a repo.
10 | #
11 | # We did this because the natively-supported allowlisting functionality in Gitleaks didn't do everything we wanted
12 | # or wasn't as robust as we needed. For example, one of the allowlisting options offered by Gitleaks depends on the line number
13 | # on which a false positive secret exists to allowlist it. (https://github.com/gitleaks/gitleaks#gitleaksignore).
14 | # This creates a fairly fragile allowlisting mechanism. This file allows us to leverage the full capabilities of the Gitleaks rule syntax
15 | # to create allowlisting functionality.
16 |
17 |
18 | [[ rules ]]
19 | id = "generic-api-key"
20 | [ rules.allowlist ]
21 | commits = [
22 | # This creates an allowlist for a UUID that was
23 | # used as an identifier, but is not secret
24 | # See https://github.com/apollographql/vscode-graphql/blob/a905280c143991b3fd675f8b4c3a7da277ccf095/packages/apollo-language-server/src/engine/index.ts#L86
25 | "a905280c143991b3fd675f8b4c3a7da277ccf095"
26 | ]
27 |
28 | [[ rules ]]
29 | id = "private-key"
30 | [ rules.allowlist ]
31 | paths = [
32 | '''sampleWorkspace/httpSchema/self-signed.key$''',
33 | ]
34 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | provenance=true
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | src/language-server/graphqlTypes.ts
2 | src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts
3 | README.md
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "singleQuote": false
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | // List of configurations. Add new configurations or edit existing ones.
4 | "configurations": [
5 | {
6 | "name": "Launch VS Code Extension",
7 | "type": "extensionHost",
8 | "request": "launch",
9 | "preLaunchTask": "BuildAndStartWorkspace",
10 | "runtimeExecutable": "${execPath}",
11 | "args": [
12 | "--extensionDevelopmentPath=${workspaceRoot}",
13 | "--disable-extensions",
14 | "${workspaceFolder}/sampleWorkspace/sampleWorkspace.code-workspace"
15 | ],
16 | "sourceMaps": true,
17 | "env": {
18 | "APOLLO_ENGINE_ENDPOINT": "http://localhost:7096/apollo",
19 | "APOLLO_FEATURE_FLAGS": "rover"
20 | //"APOLLO_ROVER_LANGUAGE_IDS": "graphql,javascript"
21 | },
22 | "outFiles": ["${workspaceRoot}/lib/**/*.js"]
23 | },
24 | {
25 | "name": "Attach to TS Server",
26 | "type": "node",
27 | "request": "attach",
28 | "protocol": "inspector",
29 | "port": 6009,
30 | "sourceMaps": true
31 | },
32 | {
33 | "name": "Extension Tests",
34 | "type": "extensionHost",
35 | "request": "launch",
36 | "runtimeExecutable": "${execPath}",
37 | "args": [
38 | "--disable-extensions",
39 | "--extensionDevelopmentPath=${workspaceFolder}",
40 | "--extensionTestsPath=${workspaceFolder}/src/__e2e__/run.js",
41 | "${workspaceFolder}/sampleWorkspace/sampleWorkspace.code-workspace"
42 | ],
43 | "outFiles": ["${workspaceFolder}/lib/**/*.js"],
44 | "preLaunchTask": "BuildAndStartWorkspace",
45 | "env": { "APOLLO_ENGINE_ENDPOINT": "http://localhost:7096/apollo" }
46 | },
47 | {
48 | "name": "Attach to Test Debugger",
49 | "type": "node",
50 | "request": "attach",
51 | "protocol": "inspector",
52 | "port": 9001,
53 | "sourceMaps": true
54 | },
55 | {
56 | "name": "Attach to CLI Debugger",
57 | "type": "node",
58 | "request": "attach",
59 | "protocol": "inspector",
60 | "port": 9002,
61 | "sourceMaps": true
62 | }
63 | ],
64 | "compounds": [
65 | {
66 | "name": "Extension + Server",
67 | "configurations": ["Launch VS Code Extension", "Attach to TS Server"]
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "editor.tabSize": 2,
4 | "editor.insertSpaces": true,
5 | "editor.rulers": [110],
6 | "editor.wordWrapColumn": 110,
7 | "files.trimTrailingWhitespace": true,
8 | "files.insertFinalNewline": true,
9 | "files.exclude": {
10 | "**/.git": true,
11 | "**/.DS_Store": true,
12 | "node_modules": false
13 | },
14 | "typescript.tsdk": "node_modules/typescript/lib",
15 | "debug.node.autoAttach": "on"
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "npm: watch",
6 | "type": "npm",
7 | "script": "watch",
8 | "problemMatcher": {
9 | "owner": "custom",
10 | "pattern": [
11 | {
12 | "regexp": "never match this please",
13 | "file": 1,
14 | "location": 2,
15 | "message": 3
16 | }
17 | ],
18 | "background": {
19 | "activeOnStart": true,
20 | "beginsPattern": "^\\s*\\[watch\\] build started.*",
21 | "endsPattern": "^\\s*\\[watch\\] build finished.*"
22 | }
23 | },
24 | "isBackground": true,
25 | "presentation": {
26 | "reveal": "never"
27 | },
28 | "group": {
29 | "kind": "build",
30 | "isDefault": true
31 | }
32 | },
33 | {
34 | "label": "sampleWorkspace",
35 | "type": "npm",
36 | "script": "sampleWorkspace:run",
37 | "isBackground": true,
38 | "problemMatcher": {
39 | "owner": "custom",
40 | "pattern": [
41 | {
42 | "regexp": "never match this please",
43 | "file": 1,
44 | "location": 2,
45 | "message": 3
46 | }
47 | ],
48 | "background": {
49 | "activeOnStart": true,
50 | "beginsPattern": "^\\s*Starting server.*",
51 | "endsPattern": "^\\s*Server ready at.*"
52 | }
53 | }
54 | },
55 | {
56 | "label": "BuildAndStartWorkspace",
57 | "dependsOn": ["npm: watch", "sampleWorkspace"]
58 | }
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .circleci
2 | .vscode
3 | .vscode-test
4 | src
5 | .env
6 | .eslintrc.js
7 | .git
8 | .gitignore
9 | .nvmrc
10 | .prettierrc
11 | .gitattributes
12 | codegen.yml
13 | jest.*.ts
14 | jest.*.js
15 | package-lock.json
16 | tsconfig.build.json
17 | tsconfig.json
18 | node_modules
19 | .git*
20 | CODEOWNERS
21 | *.tsbuildinfo
22 | sampleWorkspace
23 | .changeset
24 | .github
25 | renovate.json
26 | images/**/*.gif
27 | images/marketplace
28 | .yalc
29 | yalc.lock
30 | start-ac.mjs
31 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This file was automatically generated by the Apollo SecOps team
2 | # Please customize this file as needed prior to merging.
3 |
4 | * @apollographql/client-typescript
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Apollo GraphQL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/codegen.yml:
--------------------------------------------------------------------------------
1 | # Would be better to get the schema from the Studio registry once it can be a public variant.
2 | schema: https://graphql.api.apollographql.com/api/graphql
3 | generates:
4 | ./src/language-server/graphqlTypes.ts:
5 | documents:
6 | - src/**/*.ts
7 | - "!src/**/__tests__**/*.ts"
8 | plugins:
9 | - typescript
10 | - typescript-operations
11 | config:
12 | avoidOptionals: true
13 |
--------------------------------------------------------------------------------
/graphql.configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "comments": {
3 | "lineComment": "#"
4 | },
5 | "brackets": [
6 | ["{", "}"],
7 | ["[", "]"],
8 | ["(", ")"]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/images/IconRun.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/images/apollo.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/images/engine-stats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/engine-stats.png
--------------------------------------------------------------------------------
/images/icon-apollo-blue-400x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/icon-apollo-blue-400x400.png
--------------------------------------------------------------------------------
/images/marketplace/apollo-wordmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/apollo-wordmark.png
--------------------------------------------------------------------------------
/images/marketplace/autocomplete.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/autocomplete.gif
--------------------------------------------------------------------------------
/images/marketplace/federation-directive-hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/federation-directive-hover.png
--------------------------------------------------------------------------------
/images/marketplace/jump-to-def.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/jump-to-def.gif
--------------------------------------------------------------------------------
/images/marketplace/perf-annotation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/perf-annotation.png
--------------------------------------------------------------------------------
/images/marketplace/stats.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/stats.gif
--------------------------------------------------------------------------------
/images/marketplace/type-info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/type-info.png
--------------------------------------------------------------------------------
/images/marketplace/warnings-and-errors.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/marketplace/warnings-and-errors.gif
--------------------------------------------------------------------------------
/images/query-with-vars.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/query-with-vars.gif
--------------------------------------------------------------------------------
/images/variable-argument-completion.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apollographql/vscode-graphql/ac03316ae58e990b84a7369373e975b9594acfb3/images/variable-argument-completion.gif
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "@jest/types";
2 |
3 | const config: Config.InitialOptions = {
4 | roots: ["/src"],
5 | snapshotFormat: {
6 | escapeString: true,
7 | printBasicPrototype: true,
8 | },
9 | testMatch: ["**/__tests__/**/*.ts"],
10 | testPathIgnorePatterns: [
11 | "/node_modules/",
12 | "/fixtures/",
13 | "/snapshotSerializers/",
14 | ],
15 | transform: {
16 | "^.+\\.(ts)$": "ts-jest",
17 | },
18 | prettierPath: null,
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/jest.e2e.config.js:
--------------------------------------------------------------------------------
1 | // see https://github.com/microsoft/vscode-test/issues/37#issuecomment-700167820
2 | const path = require("path");
3 |
4 | module.exports = {
5 | moduleFileExtensions: ["js", "ts"],
6 | // restrict the roots here so jest doesn't complain about *other* snapshots it sees as obsolete
7 | roots: ["/src/language-server/__e2e__"],
8 | testMatch: ["/src/**/*.e2e.ts"],
9 | testEnvironment: "./src/__e2e__/vscode-environment.js",
10 | setupFiles: ["./src/__e2e__/setup.js"],
11 | verbose: true,
12 | moduleNameMapper: {
13 | vscode: path.join(__dirname, "src", "__e2e__", "vscode.js"),
14 | },
15 | transform: {
16 | "^.+\\.(ts)$": "ts-jest",
17 | },
18 | prettierPath: null,
19 | };
20 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["apollo-open-source"],
3 | "dependencyDashboard": true,
4 | "packageRules": [
5 | {
6 | "groupName": "all @types",
7 | "groupSlug": "all-types",
8 | "matchPackageNames": ["/@types/*/"]
9 | },
10 | {
11 | "groupName": "all devDependencies",
12 | "groupSlug": "all-dev",
13 | "matchDepTypes": ["devDependencies"],
14 | "matchPackageNames": ["*"]
15 | },
16 | {
17 | "groupName": "all dependencies - patch updates",
18 | "groupSlug": "all-patch",
19 | "matchUpdateTypes": ["patch"],
20 | "matchPackageNames": ["*"]
21 | }
22 | ],
23 | "ignoreDeps": [
24 | "@types/node",
25 | "@types/vscode",
26 | "@typescript-eslint/eslint-plugin",
27 | "@typescript-eslint/parser",
28 | "eslint",
29 | "fractional-indexing"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/sampleWorkspace/clientSchema/apollo.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | client: {
3 | service: {
4 | name: "clientSchema",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | includes: ["./src/**/*.js", "./src/**/*.ts", "./src/**/*.tsx"],
8 | excludes: ["**/__tests__/**"],
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/sampleWorkspace/clientSchema/src/clientSchema.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | extend type Droid {
4 | """
5 | A client-side addition
6 | """
7 | model: String @deprecated(reason: "It's just a robot...")
8 | }
9 |
10 | extend type Query {
11 | """
12 | Whether to use defer
13 | """
14 | featureFlagDefer: Boolean!
15 | }
16 | `;
17 |
--------------------------------------------------------------------------------
/sampleWorkspace/clientSchema/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test($defer: Boolean!) {
4 | featureFlagDefer @client(always: false) @export(as: "defer")
5 | droid(id: "2000") {
6 | name
7 | model @client
8 | primaryFunction @nonreactive
9 | ... @defer(if: $defer, label: "fc") {
10 | friendsConnection(after: 0, first: 3) @connection(key: "feed") {
11 | friends {
12 | id
13 | }
14 | }
15 | }
16 | }
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/sampleWorkspace/clientSchema/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/cjsConfig/apollo.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | client: {
3 | service: {
4 | name: "cjsConfig",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/cjsConfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/cjsConfig/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/cjsConfig/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithCJS/apollo.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | client: {
3 | service: {
4 | name: "jsConfigWithCJS",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithCJS/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithCJS/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithCJS/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithESM/apollo.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | client: {
3 | service: {
4 | name: "jsConfigWithESM",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithESM/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "type": "commonjs"
4 | }
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithESM/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/jsConfigWithESM/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/mjsConfig/apollo.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | client: {
3 | service: {
4 | name: "mjsConfig",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/mjsConfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "type": "commonjs"
4 | }
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/mjsConfig/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/mjsConfig/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithCJS/apollo.config.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | client: {
3 | service: {
4 | name: "tsConfigWithCJS",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithCJS/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "type": "module"
4 | }
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithCJS/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithCJS/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithESM/apollo.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | client: {
3 | service: {
4 | name: "tsConfigWithESM",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithESM/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "type": "commonjs"
4 | }
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithESM/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/configFileTypes/tsConfigWithESM/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/httpSchema/apollo.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | client: {
3 | service: {
4 | name: "httpSchema",
5 | url: "http://localhost:7096/graphql",
6 | // url: "https://localhost:7097/graphql",
7 | // skipSSLValidation: true,
8 | },
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/sampleWorkspace/httpSchema/self-signed.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDkzCCAnugAwIBAgIUVNlDGdat5znvwWhOEFQLq7BWzNwwDQYJKoZIhvcNAQEL
3 | BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
5 | DTI0MDkwMjA4MTgwNloXDTM0MDgzMTA4MTgwNlowWTELMAkGA1UEBhMCQVUxEzAR
6 | BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
7 | IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
8 | MIIBCgKCAQEAvHLw9Ey0WoRLxjVIajxVAT4za1pUQw3E9mtl57DPHDcdGWR56S9w
9 | KPjmND5lrtALZ5K+EKqslJdPf6Uxzxpf6phVnwpFwq5hPeY/Gpm77HxpPiJ61Q9r
10 | fsYnLtGXiZta0kbdrisALB+3QykEHOerDUF3wGiVYVcpDu7WF/WcLaF+zUlgf1gQ
11 | RTa5B3HpdCk34LiKPm9IZpWRpgLC90ro+HP+nBo7FoLYwu+WiPxg49qWEUY8fk+d
12 | TuJVdH7lf8GxcfM2oCzhBGpT5O/t6lqYBkgZvvY3YAERmAxg/OSeuUa6ChOMLK2T
13 | +2MRLy7eLaaeTmPMFjjrzFODCA2/ekfGVwIDAQABo1MwUTAdBgNVHQ4EFgQUpEu/
14 | mbpTwcOGdPZ8NhTl1e16G84wHwYDVR0jBBgwFoAUpEu/mbpTwcOGdPZ8NhTl1e16
15 | G84wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhfSAl9VFS+Fk
16 | Wkmsl1ndoXvLWVDy36YepbNfXUeB+iwebIgdyZhNu6VdpcHsPNBuUV4wr4mgZ7PK
17 | Ksp3kKdqTDTAfBzHNBiOK7QgGyrDQJa0/Nn8ifmS+TYYCOs4FnkOXCUFipXCCMaS
18 | KzFYc9Ls4jtAxiSN58NmwxW9fqRHqwHW4o2Z/aNx4EnCEat0x4QcAqq/qfEodmjH
19 | jI7/AKb4UE+yEcJnZSlUDdpM4zPM3FcjmY7JVyfd/CziywR7rHGbLz7XQcCkYyDv
20 | 5xqz0Lvk0ZtOC73cFWS41qfh8lrt34CNPoG7EaPFf+tMwhvjNooDHMQCb8y1A0Y3
21 | 2yaDZNbCbQ==
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/sampleWorkspace/httpSchema/self-signed.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8cvD0TLRahEvG
3 | NUhqPFUBPjNrWlRDDcT2a2XnsM8cNx0ZZHnpL3Ao+OY0PmWu0Atnkr4QqqyUl09/
4 | pTHPGl/qmFWfCkXCrmE95j8ambvsfGk+InrVD2t+xicu0ZeJm1rSRt2uKwAsH7dD
5 | KQQc56sNQXfAaJVhVykO7tYX9ZwtoX7NSWB/WBBFNrkHcel0KTfguIo+b0hmlZGm
6 | AsL3Suj4c/6cGjsWgtjC75aI/GDj2pYRRjx+T51O4lV0fuV/wbFx8zagLOEEalPk
7 | 7+3qWpgGSBm+9jdgARGYDGD85J65RroKE4wsrZP7YxEvLt4tpp5OY8wWOOvMU4MI
8 | Db96R8ZXAgMBAAECggEAGRrrnHLZi2jY2sDUyF5dlAr6zlWcKHYITtceSNWmXyO9
9 | Ky8BChPen9QPgFdDCZ0VX94jQaooeqqGa0K71ijf2ADPq0ky46LX4+dYHHhC762K
10 | rGiV2kDceROh5bFYvFAniHWWE8gOalJsAjT6eMqo4DJgEeXSPMO1UxQguSlofdrX
11 | 9PIkRmsmQVmVh17V4RJhhW/qg8r75OwpM1uFTknikaXwd9Rw5/HZhW4hXP5EeM2x
12 | rcaYtXudhUyG/AWCrRPpHdGGNpHPmpwKUw3uADYAb2Hicjswx34kmWAbcwVJMs8d
13 | 3QR/4hyrJnVSgYAB8/5oiNsnvaF/sO6/9KkDFpF03QKBgQDxrVJznfA0xgeVTGxt
14 | sLRhbsUyVn5tHteEdbvbTGLfl80Dayzlht9rTrjYXgQUevw40+chQW8LPEA8gmyM
15 | oCyAMouk+DJ5rl75jQnQh1/pob+ReyFi7Jl4D6Ro8J6gP6nds+wZ/BN5WLNtd/KI
16 | BMwi4fEyKjT7nKTVFNQlrZEG6wKBgQDHng2nFYbSEKv1lb5HabSxJ1bbTHld0lzI
17 | tn5zEmZ9PW6jBM0UJMEmkPRAvzhGGnzbRM0hYhiZR1FBR9T6BCJb+1N0HfT0Xo2X
18 | MTOuz8auLRtH73SCRbRoxVbz+TFmLVQuwAXdwIT+p4AgEqoJ+QyMKwuwr70AGZmm
19 | SkL08Bp7RQKBgQDYZfOgJtmAx5jerFGiXkkFvSPBkQUfPDCKIMmW8WzO/KPL3dmT
20 | pBLFiPWmd3h7xiu1zrf0ZRzDGK4EAFymBn4SRDAaBUtc/S95kDoriCvvjK912qTo
21 | aSZ6BLeYZ2wB3T+CjqpoEfh1/WCcMnzuIi2PRnSsEHLkoTxOt5nGKwXjBQKBgDve
22 | o6mhQzZt2aVmrBMvGQqpCdvsK9p/5WQtl+9bbXHSowQxxHBuNaAjiZ6Bu5cLCreZ
23 | Aw0oJsiSI0S5Dp+N7eA4mOcStQ017rGSCDY+CxDiZnRE1WTdEyb5SQMTkkVbAwyi
24 | ex/vRfQ6uKrl7infkGvZ3T+49a65/uNpEnv0J30hAoGBAJwhlCGf3BhMPrXjXGsp
25 | qxtCAbnaARErq0cI/nP6EKv8U0zOte09j/KKqFhckzsN3Df+P7dSuciIbFEDFY8Y
26 | aWVRMDF/owMo7qrb/Hyvt72gRtRAeypN1Zf7Uy7JtP7PMqdvMynK0xSdrGAl4UCV
27 | 6AOSsKQotsgA2R1PKV89Rn2R
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/sampleWorkspace/httpSchema/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | books {
5 | title
6 | author
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/sampleWorkspace/localSchema/apollo.config.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | client: {
3 | service: {
4 | name: "localSchema",
5 | localSchemaFile: "./starwarsSchema.graphql",
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/sampleWorkspace/localSchema/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | name
6 | }
7 | }
8 | `;
9 |
10 | // prettier-ignore
11 | const verylonglala = gql`type Foo { baaaaaar: String }`
12 |
--------------------------------------------------------------------------------
/sampleWorkspace/localSchema/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/localSchemaArray/apollo.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "client": {
3 | "service": {
4 | // test
5 | "name": "localMultiSchema",
6 | "localSchemaFile": ["./starwarsSchema.graphql", "./planets.graphql"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sampleWorkspace/localSchemaArray/planets.graphql:
--------------------------------------------------------------------------------
1 | """
2 | The query type, represents all of the entry points into our object graph
3 | """
4 | type Query {
5 | planets: [Planet]
6 | }
7 | """
8 | The planets mentioned in the Star Wars trilogy
9 | """
10 | type Planet {
11 | """
12 | Id of the planet
13 | """
14 | id: ID!
15 |
16 | """
17 | Name of the planet
18 | """
19 | name: String!
20 | }
21 |
--------------------------------------------------------------------------------
/sampleWorkspace/localSchemaArray/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query Test {
4 | droid(id: "2000") {
5 | dName: name
6 | }
7 | planets {
8 | id
9 | name
10 | }
11 | }
12 | `;
13 |
--------------------------------------------------------------------------------
/sampleWorkspace/localSchemaArray/starwarsSchema.graphql:
--------------------------------------------------------------------------------
1 | ../fixtures/starwarsSchema.graphql
--------------------------------------------------------------------------------
/sampleWorkspace/rover/apollo.config.yaml:
--------------------------------------------------------------------------------
1 | rover:
2 | profile: VSCode-E2E
3 | bin: ../../node_modules/@apollo/rover/binary/rover-0.27.0
4 |
--------------------------------------------------------------------------------
/sampleWorkspace/rover/src/test.graphql:
--------------------------------------------------------------------------------
1 | extend schema
2 | @link(
3 | url: "https://specs.apollo.dev/federation/v2.8"
4 | import: ["@key", "@override", "@requires", "@external", "@shareable"]
5 | )
6 |
7 | type Query {
8 | a: A
9 | }
10 |
11 | type A @key(fields: "a") {
12 | a: ID @override(from: "DNE")
13 | b: String! @requires(fields: "c") @shareable
14 | c: String! @external
15 | }
16 |
--------------------------------------------------------------------------------
/sampleWorkspace/rover/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 |
3 | sdfsdfs;
4 | gql`
5 | """
6 | The query type, represents all of the entry points into our object graph
7 | """
8 | type Query {
9 | me: User!
10 | }
11 |
12 | """
13 | Test
14 | """
15 | type User {
16 | id: ID!
17 | name: String!
18 | }
19 | `;
20 |
21 | console.log("foobar!");
22 |
23 | gql`
24 | type User {
25 | lastName: String!
26 | }
27 | `;
28 |
29 | // prettier-ignore
30 | const verylonglala = gql`type Foo { baaaaaar: String }`
31 |
--------------------------------------------------------------------------------
/sampleWorkspace/rover/supergraph.yaml:
--------------------------------------------------------------------------------
1 | subgraphs:
2 | subgraph:
3 | schema:
4 | file: src/test.graphql
5 |
--------------------------------------------------------------------------------
/sampleWorkspace/sampleWorkspace.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "localSchema",
5 | },
6 | {
7 | "path": "clientSchema",
8 | },
9 | {
10 | "path": "spotifyGraph",
11 | },
12 | {
13 | "path": "httpSchema",
14 | },
15 | {
16 | "path": "localSchemaArray",
17 | },
18 | {
19 | "path": "rover",
20 | },
21 | {
22 | "path": "configFileTypes",
23 | },
24 | {
25 | "path": "../src/language-server/__tests__/fixtures/documents",
26 | },
27 | ],
28 | "settings": {
29 | "apollographql.devTools.showPanel": "detect",
30 | "apollographql.devTools.serverPort": 7095,
31 | },
32 | }
33 |
--------------------------------------------------------------------------------
/sampleWorkspace/spotifyGraph/apollo.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | client: {
3 | service: "spotify-demo-graph-519427f5@main",
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/sampleWorkspace/spotifyGraph/src/test.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 | gql`
3 | query CurrentUserQuery {
4 | me {
5 | profile {
6 | id
7 | displayName
8 | }
9 | }
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/schemas/supergraph_config_schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "SupergraphConfig",
4 | "description": "The configuration for a single supergraph composed of multiple subgraphs.",
5 | "type": "object",
6 | "required": ["subgraphs"],
7 | "properties": {
8 | "federation_version": {
9 | "anyOf": [
10 | {
11 | "$ref": "#/definitions/FederationVersion"
12 | },
13 | {
14 | "type": "null"
15 | }
16 | ]
17 | },
18 | "subgraphs": {
19 | "type": "object",
20 | "additionalProperties": {
21 | "$ref": "#/definitions/SubgraphConfig"
22 | }
23 | }
24 | },
25 | "definitions": {
26 | "FederationVersion": {
27 | "pattern": "^(1|2|=2\\.\\d+\\.\\d+.*)$"
28 | },
29 | "SchemaSource": {
30 | "description": "Options for getting SDL: the graph registry, a file, or an introspection URL.\n\nNOTE: Introspection strips all comments and directives from the SDL.",
31 | "anyOf": [
32 | {
33 | "type": "object",
34 | "required": ["file"],
35 | "properties": {
36 | "file": {
37 | "type": "string"
38 | }
39 | }
40 | },
41 | {
42 | "type": "object",
43 | "required": ["subgraph_url"],
44 | "properties": {
45 | "introspection_headers": {
46 | "type": ["object", "null"],
47 | "additionalProperties": {
48 | "type": "string"
49 | }
50 | },
51 | "subgraph_url": {
52 | "type": "string",
53 | "format": "uri"
54 | }
55 | }
56 | },
57 | {
58 | "type": "object",
59 | "required": ["graphref", "subgraph"],
60 | "properties": {
61 | "graphref": {
62 | "type": "string"
63 | },
64 | "subgraph": {
65 | "type": "string"
66 | }
67 | }
68 | },
69 | {
70 | "type": "object",
71 | "required": ["sdl"],
72 | "properties": {
73 | "sdl": {
74 | "type": "string"
75 | }
76 | }
77 | }
78 | ]
79 | },
80 | "SubgraphConfig": {
81 | "description": "Config for a single [subgraph](https://www.apollographql.com/docs/federation/subgraphs/)",
82 | "type": "object",
83 | "required": ["schema"],
84 | "properties": {
85 | "routing_url": {
86 | "description": "The routing URL for the subgraph. This will appear in supergraph SDL and instructs the graph router to send all requests for this subgraph to this URL.",
87 | "type": ["string", "null"]
88 | },
89 | "schema": {
90 | "description": "The location of the subgraph's SDL",
91 | "allOf": [
92 | {
93 | "$ref": "#/definitions/SchemaSource"
94 | }
95 | ]
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/__e2e__/mockServer.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const {
3 | parseRequestParams,
4 | createHandler,
5 | } = require("graphql-http/lib/use/http");
6 | const { buildSchema } = require("graphql");
7 | const { Trie } = require("@wry/trie");
8 | const { readFileSync } = require("fs");
9 | const { join } = require("path");
10 |
11 | async function runMockServer(
12 | /** @type {number} */ port,
13 | useSelfSignedCert = false,
14 | onStart = (/** @type {string} */ baseUri) => {},
15 | ) {
16 | const mocks = new Trie(false);
17 |
18 | /**
19 | *
20 | * @param {import('node:http').RequestListener} listener
21 | * @returns
22 | */
23 | function createServer(listener) {
24 | if (useSelfSignedCert) {
25 | return require("node:https").createServer(
26 | {
27 | key: readFileSync(
28 | join(__dirname, "../../sampleWorkspace/httpSchema/self-signed.key"),
29 | ),
30 | cert: readFileSync(
31 | join(__dirname, "../../sampleWorkspace/httpSchema/self-signed.crt"),
32 | ),
33 | },
34 | listener,
35 | );
36 | }
37 | return require("node:http").createServer(listener);
38 | }
39 |
40 | const server = createServer(async (req, res) => {
41 | if (req.url === "/apollo") {
42 | if (req.method === "POST") {
43 | await handleApolloPost(req, res);
44 | } else if (req.method === "PUT") {
45 | await handleApolloPut(req, res);
46 | }
47 | } else if (req.url === "/graphql") {
48 | schemaHandler(req, res);
49 | } else {
50 | res.writeHead(404).end();
51 | }
52 | });
53 |
54 | server.on("error", (err) => {
55 | console.log("Failed to start server", err);
56 | });
57 |
58 | console.log("Starting server...");
59 | server.listen(port);
60 | const baseUri = `${useSelfSignedCert ? "https" : "http"}://localhost:${port}`;
61 | await onStart(baseUri);
62 | console.log(`Server ready at: ${baseUri}`);
63 | return {
64 | [Symbol.dispose]() {
65 | console.log("Closing server...");
66 | server.close();
67 | console.log("Server closed");
68 | },
69 | };
70 |
71 | /**
72 | * Mock GraphQL Endpoint Handler
73 | * @param {import('node:http').IncomingMessage} req
74 | * @param {import('node:http').ServerResponse} res
75 | */
76 | async function handleApolloPost(req, res) {
77 | const { operationName, variables } =
78 | /** @type{import("graphql-http/lib/common").RequestParams} */ (
79 | await parseRequestParams(req, res)
80 | );
81 |
82 | const mock = mocks.peek(operationName, JSON.stringify(variables));
83 | if (mock) {
84 | res.writeHead(200, { "Content-Type": "application/json" });
85 | res.end(JSON.stringify(mock.response));
86 | } else {
87 | console.warn("No mock available for %o", {
88 | operationName,
89 | variables,
90 | });
91 | res.writeHead(200).end(
92 | JSON.stringify({
93 | data: null,
94 | errors: [
95 | {
96 | message: "No mock found.",
97 | extensions: { operationName, variables },
98 | },
99 | ],
100 | }),
101 | );
102 | }
103 | }
104 |
105 | /**
106 | * Handler to accept new GraphQL Mocks
107 | * @param {import('node:http').IncomingMessage} req
108 | * @param {import('node:http').ServerResponse} res
109 | */
110 | async function handleApolloPut(req, res) {
111 | const body = await new Promise((resolve) => {
112 | let body = "";
113 | req.setEncoding("utf-8");
114 | req.on("data", (chunk) => (body += chunk));
115 | req.on("end", () => resolve(body));
116 | });
117 | const { operationName, variables, response } = JSON.parse(body);
118 | mocks.lookup(operationName, JSON.stringify(variables)).response = response;
119 | //console.info("mock loaded", { operationName, variables });
120 | res.end();
121 | }
122 | }
123 |
124 | const schema = buildSchema(`#graphql
125 | type Book {
126 | title: String
127 | author: String
128 | }
129 |
130 | type Query {
131 | books: [Book]
132 | }
133 | `);
134 | const schemaHandler = createHandler({
135 | schema,
136 | });
137 |
138 | if (require.main === module) {
139 | runMockServer(7096, false, require("./mocks.js").loadDefaultMocks);
140 | runMockServer(7097, true, require("./mocks.js").loadDefaultMocks);
141 | }
142 |
143 | module.exports.runMockServer = runMockServer;
144 |
--------------------------------------------------------------------------------
/src/__e2e__/run.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { runCLI } = require("@jest/core");
3 | const yargs = require("yargs");
4 | const { yargsOptions } = require("jest-cli");
5 |
6 | async function run() {
7 | const root = path.join(__dirname, "..", "..");
8 | const argv = await yargs(JSON.parse(process.env.TEST_ARGV || "[]").slice(2))
9 | .alias("help", "h")
10 | .options(yargsOptions).argv;
11 |
12 | const results = await runCLI(
13 | {
14 | ...argv,
15 | config: path.join(root, "jest.e2e.config.js"),
16 | runInBand: true,
17 | },
18 | [root],
19 | );
20 | process.exit(results.results.numFailedTests);
21 | }
22 |
23 | module.exports.run = run;
24 |
--------------------------------------------------------------------------------
/src/__e2e__/runTests.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const path = require("path");
3 | const { runTests } = require("@vscode/test-electron");
4 | const { runMockServer } = require("./mockServer.js");
5 | const { loadDefaultMocks } = require("./mocks.js");
6 |
7 | async function main() {
8 | const disposables = /**{@type Disposable[]}*/ [];
9 | try {
10 | // The folder containing the Extension Manifest package.json
11 | // Passed to `--extensionDevelopmentPath`
12 | const extensionDevelopmentPath = path.resolve(__dirname, "../../");
13 | // The path to the extension test runner script
14 | // Passed to --extensionTestsPath
15 | const extensionTestsPath = path.resolve(__dirname, "./run.js");
16 | process.env.TEST_ARGV = JSON.stringify(process.argv);
17 |
18 | const TEST_PORT = 7096;
19 | process.env.APOLLO_ENGINE_ENDPOINT = "http://localhost:7096/apollo";
20 | process.env.MOCK_SERVER_PORT = String(TEST_PORT);
21 | disposables.push(
22 | ...(await Promise.all([
23 | runMockServer(TEST_PORT, false, loadDefaultMocks),
24 | runMockServer(TEST_PORT + 1, true, loadDefaultMocks),
25 | ])),
26 | );
27 | // Download VS Code, unzip it and run the integration test
28 | const exitCode = await runTests({
29 | extensionDevelopmentPath,
30 | extensionTestsPath,
31 | version: process.env.VSCODE_VERSION || "stable",
32 | launchArgs: [
33 | "--disable-extensions",
34 | `${extensionDevelopmentPath}/sampleWorkspace/sampleWorkspace.code-workspace`,
35 | ],
36 | });
37 | process.exit(exitCode);
38 | } catch (err) {
39 | console.error(err);
40 | console.error("Failed to run tests");
41 | process.exit(1);
42 | } finally {
43 | disposables.forEach((d) => d[Symbol.dispose]());
44 | }
45 | }
46 |
47 | main();
48 |
--------------------------------------------------------------------------------
/src/__e2e__/setup.js:
--------------------------------------------------------------------------------
1 | jest.setTimeout(10000);
2 |
--------------------------------------------------------------------------------
/src/__e2e__/vscode-environment.js:
--------------------------------------------------------------------------------
1 | const { TestEnvironment } = require("jest-environment-node");
2 | const vscode = require("vscode");
3 |
4 | class VsCodeEnvironment extends TestEnvironment {
5 | async setup() {
6 | await super.setup();
7 | this.global.vscode = vscode;
8 | }
9 |
10 | async teardown() {
11 | this.global.vscode = {};
12 | await super.teardown();
13 | }
14 | }
15 |
16 | module.exports = VsCodeEnvironment;
17 |
--------------------------------------------------------------------------------
/src/__e2e__/vscode.js:
--------------------------------------------------------------------------------
1 | module.exports = global.vscode;
2 |
--------------------------------------------------------------------------------
/src/__mocks__/fs.js:
--------------------------------------------------------------------------------
1 | const { fs } = require("memfs");
2 |
3 | module.exports = fs;
4 |
--------------------------------------------------------------------------------
/src/__tests__/statusBar.test.ts:
--------------------------------------------------------------------------------
1 | // import StatusBar from "../statusBar";
2 |
3 | // TODO (jgzuke) disable this until we migrate away from `vscode`.
4 | // https://code.visualstudio.com/api/working-with-extensions/testing-extension#migrating-from-vscode
5 | describe.skip("statusBar", () => {
6 | it("only shows loaded state when it's supposed to", () => {
7 | // const statusBar = new StatusBar({ hasActiveTextEditor: true });
8 | // expect(statusBar.statusBarItem.text).toEqual(StatusBar.loadingStateText);
9 | // statusBar.showLoadedState({ hasActiveTextEditor: true });
10 | // expect(statusBar.statusBarItem.text).toEqual(StatusBar.loadedStateText);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/build.js:
--------------------------------------------------------------------------------
1 | const esbuild = require("esbuild");
2 | const { zodToJsonSchema } = require("zod-to-json-schema");
3 | const { writeFileSync } = require("fs");
4 | const importFresh = require("import-fresh");
5 |
6 | const production = process.argv.includes("--production");
7 | const watch = process.argv.includes("--watch");
8 |
9 | async function main() {
10 | const ctx = await esbuild.context({
11 | entryPoints: [
12 | "src/extension.ts",
13 | "src/language-server/server.ts",
14 | {
15 | in: require.resolve("@apollo/client-devtools-vscode/panel"),
16 | out: "panel",
17 | },
18 | "src/language-server/config/config.ts",
19 | "src/language-server/config/cache-busting-resolver.js",
20 | ],
21 | bundle: true,
22 | format: "cjs",
23 | minify: production,
24 | keepNames: true,
25 | sourcemap: !production,
26 | sourcesContent: false,
27 | platform: "node",
28 | outdir: "lib",
29 | external: ["vscode"],
30 | logLevel: "silent",
31 | plugins: [
32 | /* add to the end of plugins array */
33 | esbuildProblemMatcherPlugin,
34 | buildJsonSchemaPlugin,
35 | resolvePlugin,
36 | ],
37 | });
38 | if (watch) {
39 | await ctx.watch();
40 | } else {
41 | await ctx.rebuild();
42 | await ctx.dispose();
43 | }
44 | }
45 |
46 | /**
47 | * @type {import('esbuild').Plugin}
48 | */
49 | const esbuildProblemMatcherPlugin = {
50 | name: "esbuild-problem-matcher",
51 |
52 | setup(build) {
53 | build.onStart(() => {
54 | console.log("[watch] build started");
55 | });
56 | build.onEnd((result) => {
57 | result.errors.forEach(({ text, location }) => {
58 | console.error(`✘ [ERROR] ${text}`);
59 | console.error(
60 | ` ${location.file}:${location.line}:${location.column}:`,
61 | );
62 | });
63 | console.log("[watch] build finished");
64 | });
65 | },
66 | };
67 |
68 | const buildJsonSchemaPlugin = {
69 | name: "build-json-schema",
70 | setup(build) {
71 | build.onEnd(() => {
72 | const {
73 | configSchema,
74 | clientConfig,
75 | roverConfig,
76 | engineConfig,
77 | baseConfig,
78 | // @ts-ignore
79 | } = importFresh("../lib/language-server/config/config.js");
80 |
81 | const jsonSchema = zodToJsonSchema(configSchema, {
82 | errorMessages: true,
83 | definitions: {
84 | clientConfig,
85 | roverConfig,
86 | engineConfig,
87 | baseConfig,
88 | },
89 | });
90 | writeFileSync(
91 | "./schemas/apollo.config.schema.json",
92 | JSON.stringify(jsonSchema, null, 2),
93 | );
94 | });
95 | },
96 | };
97 |
98 | const resolvePlugin = {
99 | name: "resolve",
100 | setup(build) {
101 | build.onResolve(
102 | { filter: /^jsonc-parser$/ },
103 | async ({ path, ...options }) => {
104 | return build.resolve("jsonc-parser/lib/esm/main.js", options);
105 | },
106 | );
107 | },
108 | };
109 |
110 | main().catch((e) => {
111 | console.error(e);
112 | process.exit(1);
113 | });
114 |
--------------------------------------------------------------------------------
/src/debug.ts:
--------------------------------------------------------------------------------
1 | import { OutputChannel } from "vscode";
2 | import { TraceValues } from "vscode-languageclient";
3 | import { format } from "node:util";
4 |
5 | /**
6 | * for errors (and other logs in debug mode) we want to print
7 | * a stack trace showing where they were thrown. This uses an
8 | * Error's stack trace, removes the three frames regarding
9 | * this file (since they're useless) and returns the rest of the trace.
10 | */
11 | const createAndTrimStackTrace = () => {
12 | let stack: string | undefined = new Error().stack;
13 | // remove the lines in the stack from _this_ function and the caller (in this file) and shorten the trace
14 | return stack && stack.split("\n").length > 2
15 | ? stack.split("\n").slice(3, 7).join("\n")
16 | : stack;
17 | };
18 |
19 | export class Debug {
20 | private static outputConsole?: OutputChannel;
21 |
22 | public static SetOutputConsole(outputConsole: OutputChannel) {
23 | this.outputConsole = outputConsole;
24 | }
25 |
26 | private static _traceLevel: Exclude = "off";
27 | public static get traceLevel(): TraceValues {
28 | return Debug._traceLevel;
29 | }
30 | public static set traceLevel(value: TraceValues | undefined) {
31 | console.log("setting trace level to", value);
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 = "messages";
36 | } else {
37 | this._traceLevel = value || "off";
38 | }
39 | }
40 |
41 | /**
42 | * Displays an info message prefixed with [INFO]
43 | */
44 | public static info(message: string, _stack?: string) {
45 | // we check for the output console in every function
46 | // since these are static functions and can be
47 | // theoretically called before the output console is set
48 | // (although that shouldn't technically be possible)
49 | if (!this.outputConsole) return;
50 | this.outputConsole.appendLine(`[INFO] ${message}`);
51 | }
52 |
53 | /**
54 | * Displays and error message prefixed with [ERROR]
55 | * Creates and shows a truncated stack trace
56 | */
57 | public static error(message: string, stack?: string) {
58 | if (!this.outputConsole) return;
59 | const stackTrace = stack || createAndTrimStackTrace();
60 | Debug.showConsole();
61 | this.outputConsole.appendLine(`[ERROR] ${message}`);
62 | stackTrace && this.outputConsole.appendLine(stackTrace);
63 | }
64 |
65 | /**
66 | * Displays and warning message prefixed with [WARN]
67 | * Does not open the output window, since these
68 | * are less urgent
69 | */
70 | public static warning(message: string, _stack?: string) {
71 | if (!this.outputConsole) return;
72 | this.outputConsole.appendLine(`[WARN] ${message}`);
73 | }
74 |
75 | public static traceMessage(
76 | short: string,
77 | verbose = short,
78 | ...verboseParams: any[]
79 | ) {
80 | if (!this.outputConsole) return;
81 | if (Debug.traceLevel === "verbose") {
82 | this.outputConsole.appendLine(
83 | `[Trace] ${format(verbose, ...verboseParams)}`,
84 | );
85 | } else if (Debug.traceLevel === "messages") {
86 | this.outputConsole.appendLine(`[Trace] ${short}`);
87 | }
88 | }
89 |
90 | public static traceVerbose(message: string, ...params: any[]) {
91 | if (!this.outputConsole) return;
92 | if (Debug.traceLevel === "verbose") {
93 | this.outputConsole.appendLine(`[Trace] ${format(message, ...params)}`);
94 | }
95 | }
96 |
97 | /**
98 | * TODO: enable error reporting and telemetry
99 | */
100 | // public static sendErrorTelemetry(message: string) {
101 | // if (Config.enableErrorTelemetry) {
102 | // let encoded = new Buffer(message).toString("base64");
103 | // http.get("" + encoded, function () {});
104 | // }
105 | // }
106 |
107 | public static clear() {
108 | if (!this.outputConsole) return;
109 | this.outputConsole.clear();
110 | this.outputConsole.dispose();
111 | }
112 |
113 | private static showConsole() {
114 | if (!this.outputConsole) return;
115 | this.outputConsole.show();
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/devtools/server.ts:
--------------------------------------------------------------------------------
1 | import { WebSocketServer } from "ws";
2 | import { Disposable } from "vscode";
3 | import { runServer } from "@apollo/client-devtools-vscode/vscode-server";
4 | import { Debug } from "../debug";
5 | import { EventEmitter } from "node:events";
6 |
7 | export const devtoolsEvents = new EventEmitter<{
8 | toDevTools: [unknown];
9 | fromDevTools: [unknown];
10 | }>();
11 | devtoolsEvents.addListener("toDevTools", (msg) => {
12 | Debug.traceMessage(
13 | `WS > DevTools: ${
14 | msg && typeof msg === "object" && "type" in msg ? msg.type : "unknown"
15 | }`,
16 | "WS > DevTools: %o",
17 | msg,
18 | );
19 | });
20 | devtoolsEvents.addListener("fromDevTools", (msg) => {
21 | Debug.traceMessage(
22 | `DevTools > WS: ${
23 | msg && typeof msg === "object" && "type" in msg ? msg.type : "unknown"
24 | }`,
25 | "DevTools > WS: %o",
26 | msg,
27 | );
28 | });
29 |
30 | let id = 1;
31 |
32 | export function sendToDevTools(message: unknown) {
33 | devtoolsEvents.emit("toDevTools", {
34 | id: `vscode-${id++}`,
35 | source: "apollo-client-devtools",
36 | type: "actor",
37 | message,
38 | });
39 | }
40 |
41 | export let serverState:
42 | | { port: false | number; disposable: Disposable }
43 | | undefined = undefined;
44 |
45 | export function startServer(port: number) {
46 | const state = {
47 | port: false as false | number,
48 | disposable: new Disposable(() => {
49 | if (wss) {
50 | wss.close();
51 | wss = null;
52 | }
53 | if (serverState === state) {
54 | serverState = undefined;
55 | }
56 | sendToDevTools({ type: "port.changed", port, listening: false });
57 | }),
58 | };
59 |
60 | if (serverState?.port === port) {
61 | // nothing to do
62 | return;
63 | }
64 | // changing port, stop the old server
65 | serverState?.disposable.dispose();
66 | serverState = state;
67 | let wss: WebSocketServer | null = new WebSocketServer({ port });
68 | wss.on("listening", () => {
69 | state.port = port;
70 | sendToDevTools({ type: "port.changed", port, listening: true });
71 | });
72 | wss.on("close", () => {
73 | state.disposable.dispose();
74 | });
75 | runServer(wss, {
76 | addListener: (listener) => {
77 | devtoolsEvents.addListener("fromDevTools", listener);
78 | return () => {
79 | devtoolsEvents.removeListener("fromDevTools", listener);
80 | };
81 | },
82 | postMessage: (message) => {
83 | devtoolsEvents.emit("toDevTools", message);
84 | },
85 | });
86 | }
87 |
--------------------------------------------------------------------------------
/src/env/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./typescript-utility-types";
2 |
--------------------------------------------------------------------------------
/src/env/typescript-utility-types.ts:
--------------------------------------------------------------------------------
1 | export type WithRequired = T & Required>;
2 | export type DeepPartial = { [P in keyof T]?: DeepPartial };
3 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/clientSchema.e2e.ts:
--------------------------------------------------------------------------------
1 | import { TextEditor } from "vscode";
2 | import {
3 | closeAllEditors,
4 | openEditor,
5 | getCompletionItems,
6 | getHover,
7 | getPositionForEditor,
8 | GetPositionFn,
9 | } from "./utils";
10 |
11 | let editor: TextEditor;
12 | let getPosition: GetPositionFn;
13 | beforeAll(async () => {
14 | closeAllEditors();
15 | editor = await openEditor("clientSchema/src/test.js");
16 | getPosition = getPositionForEditor(editor);
17 | });
18 |
19 | test("completion", async () => {
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 | });
38 | });
39 |
40 | test("hover", async () => {
41 | expect(await getHover(editor, getPosition("featu|reFlagDefer")))
42 | .toMatchInlineSnapshot(`
43 | "\`\`\`graphql
44 | Query.featureFlagDefer: Boolean!
45 | \`\`\`
46 |
47 | ---
48 |
49 | \`Client-Only Field\` \`Resolved locally\`
50 |
51 | ---
52 |
53 | Whether to use defer"
54 | `);
55 |
56 | expect(await getHover(editor, getPosition("@c|lient(always: false)")))
57 | .toMatchInlineSnapshot(`
58 | "\`\`\`graphql
59 | @client(always: Boolean)
60 | \`\`\`
61 |
62 | ---
63 |
64 | Direct the client to resolve this field locally, either from the cache or local resolvers."
65 | `);
66 |
67 | expect(await getHover(editor, getPosition("@client(alwa|ys: false)")))
68 | .toMatchInlineSnapshot(`
69 | "\`\`\`graphql
70 | always: Boolean
71 | \`\`\`
72 |
73 | ---
74 |
75 | When true, the client will never use the cache for this value. See
76 | https://www.apollographql.com/docs/react/local-state/local-resolvers/#forcing-resolvers-with-clientalways-true"
77 | `);
78 |
79 | expect(await getHover(editor, getPosition('@expo|rt(as: "defer")')))
80 | .toMatchInlineSnapshot(`
81 | "\`\`\`graphql
82 | @export(as: String!)
83 | \`\`\`
84 |
85 | ---
86 |
87 | Export this locally resolved field as a variable to be used in the remainder of this query. See
88 | https://www.apollographql.com/docs/react/local-state/local-resolvers/#using-client-fields-as-variables"
89 | `);
90 |
91 | expect(await getHover(editor, getPosition('@export(a|s: "defer")')))
92 | .toMatchInlineSnapshot(`
93 | "\`\`\`graphql
94 | as: String!
95 | \`\`\`
96 |
97 | ---
98 |
99 | The variable name to export this field as."
100 | `);
101 |
102 | expect(await getHover(editor, getPosition("@nonre|active")))
103 | .toMatchInlineSnapshot(`
104 | "\`\`\`graphql
105 | @nonreactive
106 | \`\`\`
107 |
108 | ---
109 |
110 | The @nonreactive directive can be used to mark query fields or fragment spreads and is used to indicate that changes to the data contained within the subtrees marked @nonreactive should not trigger rerendering.
111 | This allows parent components to fetch data to be rendered by their children without rerendering themselves when the data corresponding with fields marked as @nonreactive change.
112 | https://www.apollographql.com/docs/react/data/directives#nonreactive"
113 | `);
114 |
115 | expect(
116 | await getHover(editor, getPosition('@def|er(if: $defer, label: "fc")')),
117 | ).toMatchInlineSnapshot(`
118 | "\`\`\`graphql
119 | @defer(if: Boolean, label: String)
120 | \`\`\`
121 |
122 | ---
123 |
124 | This directive enables your queries to receive data for specific fields incrementally, instead of receiving all field data at the same time.
125 | This is helpful whenever some fields in a query take much longer to resolve than others.
126 | https://www.apollographql.com/docs/react/data/directives#defer"
127 | `);
128 |
129 | expect(
130 | await getHover(editor, getPosition('@defer(i|f: $defer, label: "fc")')),
131 | ).toMatchInlineSnapshot(`
132 | "\`\`\`graphql
133 | if: Boolean
134 | \`\`\`
135 |
136 | ---
137 |
138 | When true fragment may be deferred, if omitted defaults to true."
139 | `);
140 |
141 | expect(
142 | await getHover(editor, getPosition('@defer(if: $defer, labe|l: "fc")')),
143 | ).toMatchInlineSnapshot(`
144 | "\`\`\`graphql
145 | label: String
146 | \`\`\`
147 |
148 | ---
149 |
150 | A unique label across all @defer and @stream directives in an operation.
151 | This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragment.
152 | If provided, the GraphQL Server must add it to the payload."
153 | `);
154 |
155 | expect(await getHover(editor, getPosition('@connec|tion(key: "feed")')))
156 | .toMatchInlineSnapshot(`
157 | "\`\`\`graphql
158 | @connection(key: String!, filter: [String!])
159 | \`\`\`
160 |
161 | ---
162 |
163 | Specify a custom store key for this result. See
164 | https://www.apollographql.com/docs/react/caching/advanced-topics/#the-connection-directive"
165 | `);
166 |
167 | expect(await getHover(editor, getPosition('@connection(ke|y: "feed")')))
168 | .toMatchInlineSnapshot(`
169 | "\`\`\`graphql
170 | key: String!
171 | \`\`\`
172 |
173 | ---
174 |
175 | Specify the store key."
176 | `);
177 | });
178 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/configFileTypes.e2e.ts:
--------------------------------------------------------------------------------
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 | )}\n`,
32 | "utf-8",
33 | );
34 | await reloadService();
35 | const stats = await waitForLSP(`configFileTypes/${project}/src/test.js`);
36 | expect(stats.serviceId).toBe(project);
37 | });
38 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/httpSchema.e2e.ts:
--------------------------------------------------------------------------------
1 | import { TextEditor } from "vscode";
2 | import {
3 | closeAllEditors,
4 | openEditor,
5 | getCompletionItems,
6 | getHover,
7 | getPositionForEditor,
8 | GetPositionFn,
9 | } from "./utils";
10 |
11 | let editor: TextEditor;
12 | let getPosition: GetPositionFn;
13 | beforeAll(async () => {
14 | closeAllEditors();
15 | editor = await openEditor("httpSchema/src/test.js");
16 | getPosition = getPositionForEditor(editor);
17 | });
18 |
19 | test("completion", async () => {
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 | });
32 | });
33 |
34 | test("hover", async () => {
35 | expect(await getHover(editor, getPosition("au|thor"))).toMatchInlineSnapshot(`
36 | "\`\`\`graphql
37 | Book.author: String
38 | \`\`\`"
39 | `);
40 | });
41 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/localSchema.e2e.ts:
--------------------------------------------------------------------------------
1 | import { TextEditor } from "vscode";
2 | import {
3 | closeAllEditors,
4 | openEditor,
5 | getCompletionItems,
6 | getHover,
7 | GetPositionFn,
8 | getPositionForEditor,
9 | } from "./utils";
10 |
11 | let editor: TextEditor;
12 | let getPosition: GetPositionFn;
13 | beforeAll(async () => {
14 | closeAllEditors();
15 | editor = await openEditor("localSchema/src/test.js");
16 | getPosition = getPositionForEditor(editor);
17 | });
18 |
19 | test("completion", async () => {
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 | });
33 |
34 | test("hover", async () => {
35 | expect(await getHover(editor, getPosition("na|me"))).toMatchInlineSnapshot(`
36 | "\`\`\`graphql
37 | Droid.name: String!
38 | \`\`\`
39 |
40 | ---
41 |
42 | What others call this droid"
43 | `);
44 | });
45 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/localSchemaArray.e2e.ts:
--------------------------------------------------------------------------------
1 | import { TextEditor } from "vscode";
2 | import {
3 | closeAllEditors,
4 | openEditor,
5 | getCompletionItems,
6 | getHover,
7 | GetPositionFn,
8 | getPositionForEditor,
9 | } from "./utils";
10 |
11 | let editor: TextEditor;
12 | let getPosition: GetPositionFn;
13 | beforeAll(async () => {
14 | closeAllEditors();
15 | editor = await openEditor("localSchemaArray/src/test.js");
16 | getPosition = getPositionForEditor(editor);
17 | });
18 |
19 | test("completion", async () => {
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 | });
38 | });
39 |
40 | test("hover", async () => {
41 | expect(await getHover(editor, getPosition("d|Name: name")))
42 | .toMatchInlineSnapshot(`
43 | "\`\`\`graphql
44 | Droid.name: String!
45 | \`\`\`
46 |
47 | ---
48 |
49 | What others call this droid"
50 | `);
51 | expect(await getHover(editor, getPosition("pl|anet"))).toMatchInlineSnapshot(`
52 | "\`\`\`graphql
53 | Query.planets: [Planet]
54 | \`\`\`"
55 | `);
56 | });
57 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/rover.e2e.ts:
--------------------------------------------------------------------------------
1 | import { TextEditor } from "vscode";
2 | import {
3 | closeAllEditors,
4 | openEditor,
5 | getCompletionItems,
6 | getHover,
7 | getPositionForEditor,
8 | GetPositionFn,
9 | getFullSemanticTokens,
10 | getDefinitions,
11 | } from "./utils";
12 |
13 | let editor: TextEditor;
14 | let getPosition: GetPositionFn;
15 | beforeAll(async () => {
16 | closeAllEditors();
17 | editor = await openEditor("rover/src/test.graphql");
18 | getPosition = getPositionForEditor(editor);
19 | });
20 |
21 | test("hover", async () => {
22 | expect(await getHover(editor, getPosition("@over|ride(from")))
23 | .toMatchInlineSnapshot(`
24 | "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.
25 |
26 | 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.
27 | ***
28 | \`\`\`graphql
29 | directive @override(from: String!, label: String) on FIELD_DEFINITION
30 | \`\`\`"
31 | `);
32 | });
33 |
34 | test("completion", async () => {
35 | expect(await getCompletionItems(editor, getPosition("@over|ride(from")))
36 | .toMatchInlineSnapshot(`
37 | [
38 | {
39 | "detail": undefined,
40 | "label": "@authenticated",
41 | },
42 | {
43 | "detail": "",
44 | "label": "@deprecated",
45 | },
46 | {
47 | "detail": "",
48 | "label": "@external",
49 | },
50 | {
51 | "detail": undefined,
52 | "label": "@inaccessible",
53 | },
54 | {
55 | "detail": "",
56 | "label": "@override(…)",
57 | },
58 | {
59 | "detail": undefined,
60 | "label": "@policy(…)",
61 | },
62 | {
63 | "detail": undefined,
64 | "label": "@provides(…)",
65 | },
66 | {
67 | "detail": "",
68 | "label": "@requires(…)",
69 | },
70 | {
71 | "detail": undefined,
72 | "label": "@requiresScopes(…)",
73 | },
74 | {
75 | "detail": "",
76 | "label": "@shareable",
77 | },
78 | {
79 | "detail": undefined,
80 | "label": "@tag(…)",
81 | },
82 | {
83 | "detail": "",
84 | "label": "@federation__authenticated",
85 | },
86 | {
87 | "detail": "",
88 | "label": "@federation__inaccessible",
89 | },
90 | {
91 | "detail": "",
92 | "label": "@federation__policy(…)",
93 | },
94 | {
95 | "detail": "",
96 | "label": "@federation__provides(…)",
97 | },
98 | {
99 | "detail": "",
100 | "label": "@federation__requiresScopes(…)",
101 | },
102 | {
103 | "detail": undefined,
104 | "label": "@federation__tag(…)",
105 | },
106 | ]
107 | `);
108 | });
109 |
110 | test("semantic tokens", async () => {
111 | const tokens = await getFullSemanticTokens(editor);
112 | expect(tokens[0]).toStrictEqual({
113 | startPosition: getPosition('fields: "|a"'),
114 | endPosition: getPosition('fields: "a|"'),
115 | tokenType: "property",
116 | tokenModifiers: [],
117 | });
118 | expect(tokens[1]).toStrictEqual({
119 | startPosition: getPosition('fields: "|c"'),
120 | endPosition: getPosition('fields: "c|"'),
121 | tokenType: "property",
122 | tokenModifiers: [],
123 | });
124 | });
125 |
126 | test("definitions", async () => {
127 | const definitions = await getDefinitions(editor, getPosition("a: |A"));
128 |
129 | expect(definitions[0].targetUri.toString()).toBe(
130 | editor.document.uri.toString(),
131 | );
132 | expect(
133 | editor.document.getText(definitions[0].targetSelectionRange!),
134 | ).toMatchInlineSnapshot(`"A"`);
135 | expect(editor.document.getText(definitions[0].targetRange))
136 | .toMatchInlineSnapshot(`
137 | "type A @key(fields: "a") {
138 | a: ID @override(from: "DNE")
139 | b: String! @requires(fields: "c") @shareable
140 | c: String! @external
141 | }"
142 | `);
143 | });
144 |
--------------------------------------------------------------------------------
/src/language-server/__e2e__/studioGraph.e2e.ts:
--------------------------------------------------------------------------------
1 | import { TextEditor } from "vscode";
2 | import {
3 | closeAllEditors,
4 | openEditor,
5 | getCompletionItems,
6 | getHover,
7 | getExtension,
8 | getOutputChannelDocument,
9 | reloadService,
10 | getPositionForEditor,
11 | } from "./utils";
12 | import mocks from "../../__e2e__/mocks.js";
13 |
14 | const mockPort = Number(process.env.MOCK_SERVER_PORT);
15 | beforeAll(async () => {
16 | closeAllEditors();
17 | });
18 |
19 | test("completion", async () => {
20 | const editor = await openEditor("spotifyGraph/src/test.js");
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 | });
34 | });
35 |
36 | test("hover", async () => {
37 | const editor = await openEditor("spotifyGraph/src/test.js");
38 | const getPosition = getPositionForEditor(editor);
39 | expect(await getHover(editor, getPosition("pr|ofile")))
40 | .toMatchInlineSnapshot(`
41 | "\`\`\`graphql
42 | CurrentUser.profile: CurrentUserProfile!
43 | \`\`\`
44 |
45 | ---
46 |
47 | Get detailed profile information about the current user (including the current user's username)."
48 | `);
49 | });
50 |
51 | test("wrong token", async () => {
52 | const baseUri = `http://localhost:${mockPort}`;
53 | try {
54 | await mocks.sendMock(baseUri, mocks.GetSchemaByTag_WRONG_TOKEN);
55 | await mocks.sendMock(baseUri, mocks.SchemaTagsAndFieldStats_WRONG_TOKEN);
56 |
57 | const ext = getExtension();
58 | ext.outputChannel.clear();
59 | const outputDoc = await getOutputChannelDocument();
60 |
61 | await reloadService();
62 | const output = outputDoc.getText();
63 |
64 | // currently, this logs twice, along with a full stracktrace, but no indication of where it came from
65 | // this should be improved on.
66 | expect(output.replace(/\s/g, "")).toContain(
67 | `
68 | [GraphQL error]: HTTP fetch failed from 'kotlin': 406: Not Acceptable
69 | [GraphQL error]: Invalid credentials provided
70 | ApolloError: HTTP fetch failed from 'kotlin': 406: Not Acceptable
71 | Invalid credentials provided
72 | at new ApolloError`.replace(/\s/g, ""),
73 | );
74 | } finally {
75 | await mocks.loadDefaultMocks(baseUri);
76 | await reloadService();
77 | }
78 | });
79 |
--------------------------------------------------------------------------------
/src/language-server/__tests__/diagnostics.test.ts:
--------------------------------------------------------------------------------
1 | import { Source, buildClientSchema } from "graphql";
2 | import { GraphQLDocument } from "../document";
3 | import { collectExecutableDefinitionDiagnositics } from "../diagnostics";
4 | import { starwarsSchema } from "./fixtures/starwarsSchema";
5 |
6 | const schema = buildClientSchema(starwarsSchema);
7 |
8 | const validDocument = new GraphQLDocument(
9 | new Source(`
10 | query HeroAndFriendsNames {
11 | hero {
12 | name
13 | friends {
14 | name
15 | }
16 | }
17 | }`),
18 | );
19 | const invalidDocument = new GraphQLDocument(
20 | new Source(`
21 | query HeroAndFriendsNames {
22 | hero {
23 | nam # Missing letter 'e'
24 | friend { # Missing letter 's'
25 | name
26 | }
27 | }
28 | }`),
29 | );
30 | const documentWithTypes = new GraphQLDocument(
31 | new Source(`
32 | type SomeType {
33 | thing: String
34 | }
35 | enum SomeEnum {
36 | THING_ONE
37 | THING_TWO
38 | }
39 | query HeroAndFriendsNames {
40 | hero {
41 | name
42 | friends {
43 | name
44 | }
45 | }
46 | }`),
47 | );
48 | const documentWithOffset = new GraphQLDocument(
49 | new Source(`query QueryWithOffset { hero { nam } }`, "testDocument", {
50 | line: 5,
51 | column: 10,
52 | }),
53 | );
54 | describe("Language server diagnostics", () => {
55 | describe("#collectExecutableDefinitionDiagnositics", () => {
56 | it("returns no diagnostics for a correct document", () => {
57 | const diagnostics = collectExecutableDefinitionDiagnositics(
58 | schema,
59 | validDocument,
60 | );
61 | expect(diagnostics.length).toEqual(0);
62 | });
63 | it("returns two diagnostics for a document with two errors", () => {
64 | const diagnostics = collectExecutableDefinitionDiagnositics(
65 | schema,
66 | invalidDocument,
67 | );
68 | expect(diagnostics.length).toEqual(2);
69 | });
70 | it("returns no diagnostics for a document that includes type definitions", () => {
71 | const diagnostics = collectExecutableDefinitionDiagnositics(
72 | schema,
73 | documentWithTypes,
74 | );
75 | expect(diagnostics.length).toEqual(0);
76 | });
77 | it("correctly offsets locations", () => {
78 | const diagnostics = collectExecutableDefinitionDiagnositics(
79 | schema,
80 | documentWithOffset,
81 | );
82 | expect(diagnostics.length).toEqual(1);
83 | expect(diagnostics[0].range.start.character).toEqual(40);
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/src/language-server/__tests__/fileSet.test.ts:
--------------------------------------------------------------------------------
1 | import { FileSet } from "../fileSet";
2 | import { URI } from "vscode-uri";
3 |
4 | describe("fileSet", () => {
5 | describe("includesFile", () => {
6 | it("matches includes starting with ./", () => {
7 | const fileSet = new FileSet({
8 | excludes: [],
9 | includes: ["./src/**/*.tsx"],
10 | rootURI: URI.parse("/project"),
11 | });
12 | const file = "file:///project/src/Component.tsx";
13 | expect(fileSet.includesFile(file)).toBe(true);
14 | });
15 |
16 | it("matches includes not starting with ./", () => {
17 | const fileSet = new FileSet({
18 | excludes: [],
19 | includes: ["src/**/*.tsx"],
20 | rootURI: URI.parse("/project"),
21 | });
22 | const file = "file:///project/src/Component.tsx";
23 | expect(fileSet.includesFile(file)).toBe(true);
24 | });
25 |
26 | it("does not match excludes starting with ./", () => {
27 | const fileSet = new FileSet({
28 | excludes: ["./src/Component.tsx"],
29 | includes: ["./src/**/*.tsx"],
30 | rootURI: URI.parse("/project"),
31 | });
32 | const file = "file:///project/src/Component.tsx";
33 | expect(fileSet.includesFile(file)).toBe(false);
34 | });
35 |
36 | it("does not match excludes not starting with ./", () => {
37 | const fileSet = new FileSet({
38 | excludes: ["src/Component.tsx"],
39 | includes: ["src/**/*.tsx"],
40 | rootURI: URI.parse("/project"),
41 | });
42 | const file = "file:///project/src/Component.tsx";
43 | expect(fileSet.includesFile(file)).toBe(false);
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/language-server/__tests__/fixtures/documents/commentWithTemplate.ts:
--------------------------------------------------------------------------------
1 | // hint for reading the snapshots generated by this file:
2 | // it is important to check that a rule doesn't only enter `meta.embedded.block.graphql`
3 | // but also leaves it again
4 |
5 | /* NormalComment */ `query Foo {test}`;
6 |
7 | // prettier-ignore
8 | {
9 | const _1 = /* GraphQL */`query Q1 {test}`;
10 | const _2 =
11 | /* GraphQL */`
12 | query Q2 {
13 | test
14 | }
15 | `;
16 | }
17 |
18 | /* GraphQL */ `query Q3 {test}`;
19 | /*GraphQL*/ `query Q4 {test}`;
20 | /** GraphQL */ `query Q5 {test}`;
21 |
22 | const _3 = /* GraphQL */ `
23 | query Q6 {
24 | test
25 | }
26 | `;
27 |
28 | // syntax highlighting cannot work in all examples after this - textmate grammars don't work over multiple lines like this
29 |
30 | /**
31 | * GraphQL
32 | */ `query Q7 {test}`;
33 |
34 | /* GraphQL */
35 | `query Q8 {test}`;
36 |
37 | /* graphql */
38 | `query Q9 {test}`;
39 |
40 | /* gql */
41 | `query Q10 {test}`;
42 |
--------------------------------------------------------------------------------
/src/language-server/__tests__/fixtures/documents/functionCall.ts:
--------------------------------------------------------------------------------
1 | // hint for reading the snapshots generated by this file:
2 | // it is important to check that a rule doesn't only enter `meta.embedded.block.graphql`
3 | // but also leaves it again
4 |
5 | declare function gql(arg: string, ...args: any[]): void;
6 | declare function foo(...args: any[]): void;
7 | declare type SomeResult = any;
8 | declare type SomeVariables = any;
9 |
10 | // for comparison - this is what a normal function all is colored
11 | foo(`query Foo { test }`);
12 |
13 | // if possible, these should look the same
14 | foo("notATemplate");
15 | gql("notATemplate");
16 | foo<"">("notATemplate");
17 | gql<"">("notATemplate");
18 |
19 | // prettier-ignore
20 | gql(`query Q1($arg: String!) { test }`)
21 |
22 | // prettier-ignore
23 | gql(`query Q2 { test }`)
24 |
25 | // prettier-ignore
26 | gql ( `query Q3 { test }` )
27 |
28 | gql(`
29 | query Q4 {
30 | test
31 | }
32 | `);
33 |
34 | // prettier-ignore
35 | gql ( `
36 | query Q5 {
37 | test
38 | }
39 | ` );
40 |
41 | // syntax highlighting cannot work in all examples after this - textmate grammars don't work over multiple lines like this
42 |
43 | gql<
44 | {
45 | test: string;
46 | },
47 | {
48 | test: string;
49 | }
50 | >(`
51 | query Q6 {
52 | test
53 | }
54 | `);
55 |
56 | // prettier-ignore
57 | gql(
58 | `
59 | query Q7 {
60 | test
61 | }
62 | `
63 | );
64 |
65 | gql(
66 | // ts-ignore
67 | `
68 | query Q8 {
69 | test
70 | }
71 | `,
72 | `query {}`,
73 | );
74 |
75 | // prettier-ignore
76 | gql<{
77 | test: string;
78 | },{
79 | test: string;
80 | }>(`query Q9 { test }`)
81 |
82 | // prettier-ignore
83 | gql(
84 | `query Q10 { test }`
85 | )
86 |
87 | // prettier-ignore
88 | gql
89 | (
90 | `query Q11 { test }`
91 | )
92 |
93 | export {};
94 |
--------------------------------------------------------------------------------
/src/language-server/__tests__/fixtures/documents/taggedTemplate.ts:
--------------------------------------------------------------------------------
1 | // hint for reading the snapshots generated by this file:
2 | // it is important to check that a rule doesn't only enter `meta.embedded.block.graphql`
3 | // but also leaves it again
4 |
5 | declare function gql(strings: TemplateStringsArray): void;
6 | declare function foo(strings: TemplateStringsArray): void;
7 | declare type SomeResult = any;
8 | declare type SomeVariables = any;
9 |
10 | // for comparison - this is what a normal function all is colored
11 | foo`query Foo { test }`;
12 | foo`query Foo { test }`;
13 |
14 | // prettier-ignore
15 | gql`query Q1 { test }`
16 |
17 | // prettier-ignore
18 | gql`query Q2 { test }`
19 |
20 | // prettier-ignore
21 | gql `
22 | query Q3 { test }`
23 |
24 | // prettier-ignore
25 | gql `query Q4 { test }`
26 |
27 | gql`
28 | query Q5 {
29 | test
30 | }
31 | `;
32 |
33 | gql`
34 | query Q6 {
35 | test
36 | }
37 | `;
38 |
39 | // prettier-ignore
40 | gql `
41 | query Q7 {
42 | test
43 | }
44 | `;
45 |
46 | // syntax highlighting cannot work in all examples after this - textmate grammars don't work over multiple lines like this
47 |
48 | // prettier-ignore
49 | gql
50 | `query Q8 { test }`
51 |
52 | // prettier-ignore
53 | gql<{
54 | test: string;
55 | },{
56 | test: string;
57 | }>`query Q9 { test }`
58 |
59 | gql<
60 | {
61 | test: string;
62 | },
63 | {
64 | test: string;
65 | }
66 | >`
67 | query Q10 {
68 | test
69 | }
70 | `;
71 |
72 | // prettier-ignore
73 | gql
74 | `
75 | query Q11 {
76 | test
77 | }
78 | `;
79 |
80 | export {};
81 |
--------------------------------------------------------------------------------
/src/language-server/__tests__/fixtures/documents/templateWithComment.ts:
--------------------------------------------------------------------------------
1 | // hint for reading the snapshots generated by this file:
2 | // it is important to check that a rule doesn't only enter `meta.embedded.block.graphql`
3 | // but also leaves it again
4 |
5 | `#NormalComment
6 | query Foo {test}`;
7 |
8 | `#graphql
9 | query Q1 {test}`;
10 |
11 | ` # graphql
12 | query Q2 {test}`;
13 |
14 | `# GraphQL
15 | query Q3 {
16 | test
17 | }`;
18 |
19 | `#gql
20 | # normal comment
21 | query Q4 {
22 | test
23 | }`;
24 |
25 | `#graphql
26 | query Q5 {
27 | test
28 | }`;
29 |
30 | // syntax highlighting cannot work in all examples after this - textmate grammars don't work over multiple lines like this
31 | `
32 |
33 | # graphql
34 |
35 | query Q6 {
36 | test
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/language-server/config/__tests__/config.ts:
--------------------------------------------------------------------------------
1 | import { ClientConfig, parseApolloConfig } from "../";
2 | import { URI } from "vscode-uri";
3 |
4 | describe("ApolloConfig", () => {
5 | describe("confifDirURI", () => {
6 | it("properly parses dir paths for configDirURI", () => {
7 | const uri = URI.parse("/test/dir/name");
8 | const config = parseApolloConfig({ client: { service: "hai" } }, uri);
9 | // can be either /test/dir/name or \\test\\dir\\name depending on platform
10 | // this difference is fine :)
11 | expect(config?.configDirURI?.fsPath).toMatch(
12 | /\/test\/dir\/name|\\test\\dir\\name/,
13 | );
14 | });
15 | it("properly parses filepaths for configDirURI", () => {
16 | const uri = URI.parse("/test/dir/name/apollo.config.js");
17 | const config = parseApolloConfig(
18 | {
19 | client: { service: "hai" },
20 | },
21 | uri,
22 | );
23 | // can be either /test/dir/name or \\test\\dir\\name depending on platform
24 | // this difference is fine :)
25 | expect(config?.configDirURI?.fsPath).toMatch(
26 | /\/test\/dir\/name|\\test\\dir\\name/,
27 | );
28 | });
29 | });
30 |
31 | describe("variant", () => {
32 | it("gets default variant when none is set", () => {
33 | const config = parseApolloConfig({
34 | client: { service: "hai" },
35 | });
36 | expect(config?.variant).toEqual("current");
37 | });
38 |
39 | it("gets variant from service specifier", () => {
40 | const config = parseApolloConfig({
41 | client: { service: "hai@master" },
42 | });
43 | expect(config?.variant).toEqual("master");
44 | });
45 |
46 | it("can set and override variants", () => {
47 | const config = parseApolloConfig({
48 | client: { service: "hai@master" },
49 | });
50 | config!.variant = "new";
51 | expect(config?.variant).toEqual("new");
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/language-server/config/__tests__/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getServiceFromKey,
3 | getGraphIdFromConfig,
4 | isClientConfig,
5 | isLocalServiceConfig,
6 | parseServiceSpecifier,
7 | parseApolloConfig,
8 | configSchema,
9 | } from "../";
10 |
11 | describe("getServiceFromKey", () => {
12 | it("returns undefined with no provided key", () => {
13 | expect(getServiceFromKey()).toBeUndefined();
14 | });
15 |
16 | it("returns service name from service api key", () => {
17 | const key = "service:bob-123:489fhseo4";
18 | expect(getServiceFromKey(key)).toEqual("bob-123");
19 | });
20 |
21 | it("returns nothing if key is not a service key", () => {
22 | const key = "not-a-service:bob-123:489fhseo4";
23 | expect(getServiceFromKey(key)).toBeUndefined();
24 | });
25 |
26 | it("returns nothing if key is malformed", () => {
27 | const key = "service/bob-123:489fhseo4";
28 | expect(getServiceFromKey(key)).toBeUndefined();
29 | });
30 | });
31 |
32 | describe("getServiceName", () => {
33 | describe("client config", () => {
34 | it("finds service name when client.service is a string", () => {
35 | const rawConfig = configSchema.parse({
36 | client: { service: "my-service" },
37 | });
38 | expect(getGraphIdFromConfig(rawConfig)).toEqual("my-service");
39 |
40 | const rawConfigWithTag = configSchema.parse({
41 | client: { service: "my-service@master" },
42 | });
43 | expect(getGraphIdFromConfig(rawConfigWithTag)).toEqual("my-service");
44 | });
45 |
46 | it("finds service name when client.service is an object", () => {
47 | const rawConfig = configSchema.parse({
48 | client: {
49 | service: { name: "my-service", localSchemaFile: "./someFile" },
50 | },
51 | });
52 | expect(getGraphIdFromConfig(rawConfig)).toEqual("my-service");
53 | });
54 | });
55 | describe("service config", () => {
56 | it("finds service name from raw service config", () => {
57 | const rawConfig = configSchema.parse({
58 | client: {
59 | service: {
60 | name: "my-service",
61 | localSchemaFile: "./someFile",
62 | },
63 | includes: [],
64 | excludes: [],
65 | },
66 | });
67 | expect(getGraphIdFromConfig(rawConfig)).toEqual("my-service");
68 | });
69 | });
70 | });
71 |
72 | describe("isClientConfig", () => {
73 | it("identifies client config properly", () => {
74 | const config = parseApolloConfig({
75 | client: { service: "hello" },
76 | });
77 | expect(isClientConfig(config!)).toBeTruthy();
78 | });
79 | });
80 |
81 | describe("isLocalServiceConfig", () => {
82 | it("properly identifies a client config that uses localSchemaFiles", () => {
83 | const clientServiceConfig = { name: "my-service", localSchemaFile: "okay" };
84 | expect(isLocalServiceConfig(clientServiceConfig)).toBeTruthy();
85 | });
86 | });
87 |
88 | describe("parseServiceSpecifier", () => {
89 | it("parses service identifier for service id and tag properly", () => {
90 | const [id, tag] = parseServiceSpecifier("my-service@master");
91 | expect(id).toEqual("my-service");
92 | expect(tag).toEqual("master");
93 |
94 | const [idFromSimpleName, tagFromSimpleName] =
95 | parseServiceSpecifier("my-service");
96 | expect(idFromSimpleName).toEqual("my-service");
97 | expect(tagFromSimpleName).toBeUndefined();
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/src/language-server/config/cache-busting-resolver.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const { pathToFileURL } = require("node:url");
3 |
4 | /** @import { ResolveContext, ResolutionResult, LoadResult, ImportContext, ImportAttributes, ImportAssertions, LegacyResolveContext, LegacyImportContext, Format } from "./cache-busting-resolver.types" */
5 |
6 | /**
7 | * importAssertions was renamed to importAttributes after following versions of Node.js.
8 | * Once we hit a minimum of v1.92 of VSCode, we can remove the legacy check and
9 | * use `importAttributes` directly.
10 | *
11 | * - v21.0.0
12 | * - v20.10.0
13 | * - v18.19.0
14 | *
15 | * @see https://github.com/apollographql/vscode-graphql/issues/225
16 | * @see https://nodejs.org/docs/latest/api/module.html#resolvespecifier-context-nextresolve
17 | *
18 | * @param {ResolveContext|ImportContext|LegacyResolveContext|LegacyImportContext} context
19 | * @returns {context is ResolveContext|ImportContext}
20 | */
21 | function isImportAttributesAvailable(context) {
22 | return "importAttributes" in context;
23 | }
24 |
25 | /**
26 | * @param {ResolveContext|ImportContext} context
27 | * @returns {"importAttributes"|"importAssertions"}
28 | */
29 | function resolveImportAttributesKeyName(context) {
30 | if (isImportAttributesAvailable(context)) {
31 | return "importAttributes";
32 | }
33 | return "importAssertions";
34 | }
35 |
36 | /**
37 | * @param {ResolveContext|ImportContext|LegacyResolveContext|LegacyImportContext} context
38 | * @returns {ImportAttributes|ImportAssertions}
39 | */
40 | function resolveImportAttributes(context) {
41 | if (isImportAttributesAvailable(context)) {
42 | return context.importAttributes;
43 | }
44 | return context.importAssertions;
45 | }
46 |
47 | /**
48 | * @param {ImportAttributes|ImportAssertions} importAttributes
49 | * @returns {Format|null}
50 | */
51 | function resolveConfigFormat(importAttributes) {
52 | const [as, format] = importAttributes.as
53 | ? importAttributes.as.split(":")
54 | : [];
55 | if (as === "cachebust" && format) {
56 | return /** @type {Format} */ (format);
57 | }
58 | return null;
59 | }
60 |
61 | /**
62 | * @param {string} specifier
63 | * @returns {string}
64 | */
65 | function bustFileName(specifier) {
66 | const url = pathToFileURL(specifier);
67 | url.pathname = url.pathname + "." + Date.now() + ".js";
68 | return url.toString();
69 | }
70 |
71 | /**
72 | *
73 | * @param {string} specifier
74 | * @param {ResolveContext} context
75 | * @param {(specifier: string,context: ResolveContext) => Promise} nextResolve
76 | * @returns {Promise}
77 | */
78 | async function resolve(specifier, context, nextResolve) {
79 | const importAttributes = resolveImportAttributes(context);
80 | const format = resolveConfigFormat(importAttributes);
81 | if (!format) {
82 | return nextResolve(specifier, context);
83 | }
84 | // no need to resolve at all, we have all necessary information
85 | return {
86 | url: bustFileName(specifier),
87 | format,
88 | [resolveImportAttributesKeyName(context)]: importAttributes,
89 | shortCircuit: true,
90 | };
91 | }
92 |
93 | /**
94 | *
95 | * @param {string} url
96 | * @param {ImportContext} context
97 | * @param {(url: string, context: ImportContext) => Promise} nextLoad
98 | * @returns {Promise}
99 | */
100 | async function load(url, context, nextLoad) {
101 | const importAttributes = resolveImportAttributes(context);
102 | const format = resolveConfigFormat(importAttributes);
103 | if (!format) {
104 | return nextLoad(url, context);
105 | }
106 | const contents =
107 | "contents" in importAttributes
108 | ? importAttributes.contents
109 | : Object.keys(importAttributes).find((key) => key != "as");
110 | return {
111 | format,
112 | shortCircuit: true,
113 | source: /** @type {string} */ (contents),
114 | };
115 | }
116 |
117 | module.exports = {
118 | resolve,
119 | load,
120 | };
121 |
--------------------------------------------------------------------------------
/src/language-server/config/cache-busting-resolver.types.ts:
--------------------------------------------------------------------------------
1 | import { pathToFileURL } from "node:url";
2 |
3 | export type ImportAttributes =
4 | | {
5 | as: `cachebust:${Format}`;
6 | contents: string;
7 | format: Format;
8 | }
9 | | { as?: undefined };
10 |
11 | export type ImportAssertions =
12 | | {
13 | as: `cachebust:${Format}`;
14 | [key: string]: string;
15 | }
16 | | { as?: undefined };
17 |
18 | export type Format =
19 | | "builtin"
20 | | "commonjs"
21 | | "json"
22 | | "module"
23 | | "wasm"
24 | | null
25 | | undefined;
26 |
27 | export interface LegacyResolveContext {
28 | conditions: string[];
29 | importAssertions: ImportAssertions;
30 | parentURL?: string;
31 | }
32 |
33 | export interface ResolveContext {
34 | conditions: string[];
35 | importAttributes: ImportAttributes;
36 | parentURL?: string;
37 | }
38 |
39 | export interface LegacyImportContext {
40 | conditions: string[];
41 | importAssertions: ImportAssertions;
42 | format: Format;
43 | }
44 | export interface ImportContext {
45 | conditions: string[];
46 | importAttributes: ImportAttributes;
47 | format: Format;
48 | }
49 |
50 | export interface ResolutionResult {
51 | format: Format;
52 | importAttributes?: ImportAttributes;
53 | shortCircuit?: boolean;
54 | url: string;
55 | }
56 |
57 | export interface LoadResult {
58 | format: Format;
59 | shortCircuit?: boolean;
60 | source: string;
61 | }
62 |
63 | export {};
64 |
--------------------------------------------------------------------------------
/src/language-server/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./utils";
2 | export * from "./config";
3 | export * from "./loadConfig";
4 |
--------------------------------------------------------------------------------
/src/language-server/config/loadConfig.ts:
--------------------------------------------------------------------------------
1 | import { cosmiconfig, defaultLoaders, Loader } from "cosmiconfig";
2 | import { dirname, resolve } from "path";
3 | import { readFileSync, existsSync, lstatSync } from "fs";
4 | import {
5 | ApolloConfig,
6 | RawApolloConfigFormat,
7 | parseApolloConfig,
8 | } from "./config";
9 | import { getServiceFromKey } from "./utils";
10 | import { URI } from "vscode-uri";
11 | import { Debug } from "../utilities";
12 | import { ParseError, parse as parseJsonC } from "jsonc-parser";
13 | import { loadJs, loadTs } from "./loadTsConfig";
14 |
15 | // config settings
16 | const MODULE_NAME = "apollo";
17 | export const supportedConfigFileNames = [
18 | "package.json",
19 | `${MODULE_NAME}.config.js`,
20 | `${MODULE_NAME}.config.ts`,
21 | `${MODULE_NAME}.config.mjs`,
22 | `${MODULE_NAME}.config.cjs`,
23 | `${MODULE_NAME}.config.yaml`,
24 | `${MODULE_NAME}.config.yml`,
25 | `${MODULE_NAME}.config.json`,
26 | ];
27 | export const envFileNames = [".env", ".env.local"];
28 |
29 | export const keyEnvVar = "APOLLO_KEY";
30 |
31 | export interface LoadConfigSettings {
32 | // the current working directory to start looking for the config
33 | // config loading only works on node so we default to
34 | // process.cwd()
35 | configPath: string;
36 | }
37 |
38 | export type ConfigResult = {
39 | config: T;
40 | filepath: string;
41 | isEmpty?: boolean;
42 | } | null;
43 |
44 | // XXX load .env files automatically
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 |
59 | export async function loadConfig({
60 | configPath,
61 | }: LoadConfigSettings): Promise {
62 | const explorer = cosmiconfig(MODULE_NAME, {
63 | searchPlaces: supportedConfigFileNames,
64 | loaders: {
65 | ...defaultLoaders,
66 | ".ts": loadTs,
67 | ".mjs": loadJs,
68 | ".cjs": loadJs,
69 | ".js": loadJs,
70 | ".json": loadJsonc,
71 | },
72 | });
73 |
74 | // search can fail if a file can't be parsed (ex: a nonsense js file) so we wrap in a try/catch
75 | let loadedConfig: ConfigResult;
76 | try {
77 | loadedConfig = (await explorer.search(
78 | configPath,
79 | )) as ConfigResult;
80 | } catch (error) {
81 | throw new Error(`A config file failed to load with options: ${JSON.stringify(
82 | arguments[0],
83 | )}.
84 | The error was: ${error}`);
85 | }
86 |
87 | if (!loadedConfig || loadedConfig.isEmpty) {
88 | Debug.error(
89 | `No Apollo config found for project or config file failed to load. For more information, please refer to: https://go.apollo.dev/t/config`,
90 | );
91 | // deliberately returning `null` here, but not throwing an error - the user may not have a config file and that's okay, it might just be a project without a graph
92 | return null;
93 | }
94 |
95 | if (loadedConfig.filepath.endsWith("package.json")) {
96 | Debug.warning(
97 | 'The "apollo" package.json configuration key will no longer be supported in Apollo v3. Please use the apollo.config.js file for Apollo project configuration. For more information, see: https://go.apollo.dev/t/config',
98 | );
99 | }
100 |
101 | // add API key from the env
102 | let apiKey, nameFromKey;
103 |
104 | // loop over the list of possible .env files and try to parse for key
105 | // and service name. Files are scanned and found values are preferred
106 | // in order of appearance in `envFileNames`.
107 | envFileNames.forEach((envFile) => {
108 | const dotEnvPath = resolve(configPath, envFile);
109 |
110 | if (existsSync(dotEnvPath) && lstatSync(dotEnvPath).isFile()) {
111 | const env: { [key: string]: string } = require("dotenv").parse(
112 | readFileSync(dotEnvPath),
113 | );
114 | apiKey = env[keyEnvVar];
115 | }
116 | });
117 |
118 | if (apiKey) {
119 | nameFromKey = getServiceFromKey(apiKey);
120 | }
121 |
122 | let { config, filepath } = loadedConfig;
123 |
124 | const finalConfig = parseApolloConfig(config, URI.file(resolve(filepath)), {
125 | apiKey,
126 | serviceName: nameFromKey,
127 | configPath: dirname(filepath),
128 | });
129 | await finalConfig.verify();
130 | return finalConfig;
131 | }
132 |
--------------------------------------------------------------------------------
/src/language-server/config/loadTsConfig.ts:
--------------------------------------------------------------------------------
1 | import { Loader } from "cosmiconfig";
2 | import { dirname, extname } from "node:path";
3 | import typescript from "typescript";
4 | import { pathToFileURL } from "node:url";
5 | import { register } from "node:module";
6 | import {
7 | ImportAssertions,
8 | ImportAttributes,
9 | } from "./cache-busting-resolver.types";
10 | // implementation based on https://github.com/cosmiconfig/cosmiconfig/blob/a5a842547c13392ebb89a485b9e56d9f37e3cbd3/src/loaders.ts
11 | // Copyright (c) 2015 David Clark licensed MIT. Full license can be found here:
12 | // https://github.com/cosmiconfig/cosmiconfig/blob/a5a842547c13392ebb89a485b9e56d9f37e3cbd3/LICENSE
13 |
14 | try {
15 | register(
16 | pathToFileURL(require.resolve("./config/cache-busting-resolver.js")),
17 | );
18 | } catch {
19 | register(pathToFileURL(require.resolve("./cache-busting-resolver.js")));
20 | }
21 |
22 | export const loadTs: Loader = async function loadTs(filepath, content) {
23 | try {
24 | return await load(filepath, content, "module", {
25 | module: typescript.ModuleKind.ES2022,
26 | });
27 | } catch (error) {
28 | if (
29 | error instanceof Error &&
30 | // [ERROR] ReferenceError: module is not defined in ES module scope
31 | error.message.includes("module is not defined")
32 | ) {
33 | return await load(filepath, content, "commonjs", {
34 | module: typescript.ModuleKind.CommonJS,
35 | });
36 | } else {
37 | throw error;
38 | }
39 | }
40 | };
41 |
42 | async function load(
43 | filepath: string,
44 | content: string,
45 | type: "module" | "commonjs",
46 | compilerOptions: Partial,
47 | ) {
48 | let transpiledContent;
49 |
50 | try {
51 | const config = resolveTsConfig(dirname(filepath)) ?? {};
52 | config.compilerOptions = {
53 | ...config.compilerOptions,
54 |
55 | moduleResolution: typescript.ModuleResolutionKind.Bundler,
56 | target: typescript.ScriptTarget.ES2022,
57 | noEmit: false,
58 | ...compilerOptions,
59 | };
60 | transpiledContent = typescript.transpileModule(content, config).outputText;
61 | } catch (error: any) {
62 | error.message = `TypeScript Error in ${filepath}:\n${error.message}`;
63 | throw error;
64 | }
65 | return loadCachebustedJs(filepath, transpiledContent, type);
66 | }
67 |
68 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
69 | function resolveTsConfig(directory: string): any {
70 | const filePath = typescript.findConfigFile(directory, (fileName) => {
71 | return typescript.sys.fileExists(fileName);
72 | });
73 | if (filePath !== undefined) {
74 | const { config, error } = typescript.readConfigFile(filePath, (path) =>
75 | typescript.sys.readFile(path),
76 | );
77 | if (error) {
78 | throw new Error(`Error in ${filePath}: ${error.messageText.toString()}`);
79 | }
80 | return config;
81 | }
82 | return;
83 | }
84 |
85 | export const loadJs: Loader = async function loadJs(filepath, contents) {
86 | const extension = extname(filepath);
87 | if (extension === ".mjs") {
88 | return loadCachebustedJs(filepath, contents, "module");
89 | }
90 | if (extension === ".cjs") {
91 | return loadCachebustedJs(filepath, contents, "commonjs");
92 | }
93 | try {
94 | return await loadCachebustedJs(filepath, contents, "module");
95 | } catch (error) {
96 | if (
97 | error instanceof Error &&
98 | // [ERROR] ReferenceError: module is not defined in ES module scope
99 | // [ERROR] ReferenceError: require is not defined in ES module scope
100 | error.message.includes("is not defined in ES module scope")
101 | ) {
102 | return loadCachebustedJs(filepath, contents, "commonjs");
103 | } else {
104 | throw error;
105 | }
106 | }
107 | };
108 |
109 | async function loadCachebustedJs(
110 | filename: string,
111 | contents: string,
112 | type: "module" | "commonjs",
113 | ) {
114 | return (
115 | await import(
116 | filename,
117 | // @ts-ignore
118 | {
119 | with: {
120 | as: `cachebust:${type}`,
121 | contents,
122 | format: type,
123 | } satisfies ImportAttributes,
124 | assert: {
125 | as: `cachebust:${type}`,
126 | contents,
127 | format: type,
128 | } satisfies ImportAssertions,
129 | }
130 | )
131 | ).default;
132 | }
133 |
--------------------------------------------------------------------------------
/src/language-server/config/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloConfig,
3 | ClientConfig,
4 | ClientServiceConfig,
5 | LocalServiceConfig,
6 | ParsedApolloConfigFormat,
7 | } from "./config";
8 | import { ServiceSpecifier, ServiceIDAndTag } from "../engine";
9 |
10 | export function isClientConfig(config: ApolloConfig): config is ClientConfig {
11 | return config instanceof ClientConfig;
12 | }
13 |
14 | // checks the `config.client.service` object for a localSchemaFile
15 | export function isLocalServiceConfig(
16 | config: ClientServiceConfig,
17 | ): config is LocalServiceConfig {
18 | return !!(config as LocalServiceConfig).localSchemaFile;
19 | }
20 |
21 | export function getServiceFromKey(key?: string) {
22 | if (key) {
23 | const [type, service] = key.split(":");
24 | if (type === "service") return service;
25 | }
26 | return;
27 | }
28 |
29 | export function getGraphIdFromConfig(config: ParsedApolloConfigFormat) {
30 | if (config.client) {
31 | if (typeof config.client.service === "string") {
32 | return parseServiceSpecifier(
33 | config.client.service as ServiceSpecifier,
34 | )[0];
35 | }
36 | return config.client.service && config.client.service.name;
37 | } else {
38 | return undefined;
39 | }
40 | }
41 |
42 | export function parseServiceSpecifier(specifier: ServiceSpecifier) {
43 | const [id, tag] = specifier.split("@").map((x) => x.trim());
44 | return [id, tag] as ServiceIDAndTag;
45 | }
46 |
--------------------------------------------------------------------------------
/src/language-server/config/which.d.ts:
--------------------------------------------------------------------------------
1 | declare module "which" {
2 | interface Options {
3 | /** Use instead of the PATH environment variable. */
4 | path?: string;
5 | /** Use instead of the PATHEXT environment variable. */
6 | pathExt?: string;
7 | /** Return all matches, instead of just the first one. Note that this means the function returns an array of strings instead of a single string. */
8 | all?: boolean;
9 | }
10 |
11 | function which(cmd: string, options?: Options): number;
12 | namespace which {
13 | function sync(
14 | cmd: string,
15 | options?: Options & { nothrow?: boolean },
16 | ): string | null;
17 | }
18 | export = which;
19 | }
20 |
--------------------------------------------------------------------------------
/src/language-server/diagnostics.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLSchema,
3 | GraphQLError,
4 | FragmentDefinitionNode,
5 | isExecutableDefinitionNode,
6 | DocumentNode,
7 | validate,
8 | NoDeprecatedCustomRule,
9 | } from "graphql";
10 |
11 | function findDeprecatedUsages(
12 | schema: GraphQLSchema,
13 | ast: DocumentNode,
14 | ): ReadonlyArray {
15 | return validate(schema, ast, [NoDeprecatedCustomRule]);
16 | }
17 |
18 | import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver/node";
19 |
20 | import { GraphQLDocument } from "./document";
21 | import { highlightNodeForNode } from "./utilities/graphql";
22 | import { rangeForASTNode } from "./utilities/source";
23 |
24 | import { getValidationErrors } from "./errors/validation";
25 | import { DocumentUri } from "./project/base";
26 | import { ValidationRule } from "graphql/validation/ValidationContext";
27 |
28 | /**
29 | * Build an array of code diagnostics for all executable definitions in a document.
30 | */
31 | export function collectExecutableDefinitionDiagnositics(
32 | schema: GraphQLSchema,
33 | queryDocument: GraphQLDocument,
34 | fragments: { [fragmentName: string]: FragmentDefinitionNode } = {},
35 | rules?: ValidationRule[],
36 | ): Diagnostic[] {
37 | const ast = queryDocument.ast;
38 | if (!ast) return queryDocument.syntaxErrors;
39 |
40 | const astWithExecutableDefinitions = {
41 | ...ast,
42 | definitions: ast.definitions.filter(isExecutableDefinitionNode),
43 | };
44 |
45 | const diagnostics = [];
46 |
47 | for (const error of getValidationErrors(
48 | schema,
49 | astWithExecutableDefinitions,
50 | fragments,
51 | rules,
52 | )) {
53 | diagnostics.push(
54 | ...diagnosticsFromError(error, DiagnosticSeverity.Error, "Validation"),
55 | );
56 | }
57 |
58 | for (const error of findDeprecatedUsages(
59 | schema,
60 | astWithExecutableDefinitions,
61 | )) {
62 | diagnostics.push(
63 | ...diagnosticsFromError(error, DiagnosticSeverity.Warning, "Deprecation"),
64 | );
65 | }
66 |
67 | return diagnostics;
68 | }
69 |
70 | export function diagnosticsFromError(
71 | error: GraphQLError,
72 | severity: DiagnosticSeverity,
73 | type: string,
74 | ): GraphQLDiagnostic[] {
75 | if (!error.nodes) {
76 | return [];
77 | }
78 |
79 | return error.nodes.map((node) => {
80 | return {
81 | source: `GraphQL: ${type}`,
82 | message: error.message,
83 | severity,
84 | range: rangeForASTNode(highlightNodeForNode(node) || node),
85 | error,
86 | };
87 | });
88 | }
89 |
90 | export interface GraphQLDiagnostic extends Diagnostic {
91 | /**
92 | * The GraphQLError that produced this Diagnostic
93 | */
94 | error: GraphQLError;
95 | }
96 |
97 | export namespace GraphQLDiagnostic {
98 | export function is(diagnostic: Diagnostic): diagnostic is GraphQLDiagnostic {
99 | return "error" in diagnostic;
100 | }
101 | }
102 |
103 | export class DiagnosticSet {
104 | private diagnosticsByFile = new Map();
105 |
106 | entries() {
107 | return this.diagnosticsByFile.entries();
108 | }
109 |
110 | addDiagnostics(uri: DocumentUri, diagnostics: Diagnostic[]) {
111 | const existingDiagnostics = this.diagnosticsByFile.get(uri);
112 | if (!existingDiagnostics) {
113 | this.diagnosticsByFile.set(uri, diagnostics);
114 | } else {
115 | existingDiagnostics.push(...diagnostics);
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/language-server/engine/index.ts:
--------------------------------------------------------------------------------
1 | import { SCHEMA_TAGS_AND_FIELD_STATS } from "./operations/schemaTagsAndFieldStats";
2 | import { FRONTEND_URL_ROOT } from "./operations/frontendUrlRoot";
3 | import {
4 | ApolloClient,
5 | ApolloLink,
6 | createHttpLink,
7 | InMemoryCache,
8 | } from "@apollo/client/core";
9 | import { setContext } from "@apollo/client/link/context";
10 | import { onError } from "@apollo/client/link/error";
11 |
12 | export interface ClientIdentity {
13 | name: string;
14 | version: string;
15 | referenceID: string;
16 | }
17 |
18 | export type ServiceID = string;
19 | export type ClientID = string;
20 | export type SchemaTag = string;
21 | export type ServiceIDAndTag = [ServiceID, SchemaTag?];
22 | export type ServiceSpecifier = string;
23 | // Map from parent type name to field name to latency in ms.
24 | export type FieldLatenciesMS = Map>;
25 |
26 | export function noServiceError(service: string | undefined, endpoint?: string) {
27 | return `Could not find graph ${
28 | service ? service : ""
29 | } from Apollo at ${endpoint}. Please check your API key and graph ID`;
30 | }
31 |
32 | export class ApolloEngineClient {
33 | public readonly client: ApolloClient;
34 | public readonly query: ApolloClient["query"];
35 |
36 | constructor(
37 | private readonly engineKey: string,
38 | baseURL: string,
39 | private readonly clientIdentity: ClientIdentity,
40 | ) {
41 | const link = ApolloLink.from([
42 | onError(({ graphQLErrors, networkError, operation }) => {
43 | const { result, response } = operation.getContext();
44 | if (graphQLErrors) {
45 | graphQLErrors.map((graphqlError) =>
46 | console.error(`[GraphQL error]: ${graphqlError.message}`),
47 | );
48 | }
49 |
50 | if (networkError) {
51 | console.log(`[Network Error]: ${networkError}`);
52 | }
53 |
54 | if (response && response.status >= 400) {
55 | console.log(`[Network Error] ${response.bodyText}`);
56 | }
57 | }),
58 | createHttpLink({
59 | uri: baseURL,
60 | headers: {
61 | ["x-api-key"]: this.engineKey,
62 | ["apollo-client-name"]: this.clientIdentity.name,
63 | ["apollo-client-reference-id"]: this.clientIdentity.referenceID,
64 | ["apollo-client-version"]: this.clientIdentity.version,
65 | },
66 | }),
67 | ]);
68 |
69 | this.client = new ApolloClient({
70 | link,
71 | cache: new InMemoryCache(),
72 | });
73 | this.query = this.client.query.bind(this.client);
74 | }
75 |
76 | async loadSchemaTagsAndFieldLatencies(serviceID: string) {
77 | const { data, errors } = await this.client.query({
78 | query: SCHEMA_TAGS_AND_FIELD_STATS,
79 | variables: {
80 | id: serviceID,
81 | },
82 | fetchPolicy: "no-cache",
83 | });
84 |
85 | if (!(data && data.service && data.service.schemaTags) || errors) {
86 | throw new Error(
87 | errors
88 | ? errors.map((error) => error.message).join("\n")
89 | : "No service returned. Make sure your service name and API key match",
90 | );
91 | }
92 |
93 | const schemaTags: string[] = data.service.schemaTags.map(
94 | ({ tag }: { tag: string }) => tag,
95 | );
96 |
97 | const fieldLatenciesMS: FieldLatenciesMS = new Map();
98 |
99 | data.service.stats.fieldLatencies.forEach((fieldLatency) => {
100 | const { parentType, fieldName } = fieldLatency.groupBy;
101 |
102 | if (!parentType || !fieldName) {
103 | return;
104 | }
105 | const fieldsMap =
106 | fieldLatenciesMS.get(parentType) ||
107 | fieldLatenciesMS.set(parentType, new Map()).get(parentType)!;
108 |
109 | fieldsMap.set(fieldName, fieldLatency.metrics.fieldHistogram.durationMs);
110 | });
111 |
112 | return { schemaTags, fieldLatenciesMS };
113 | }
114 |
115 | async loadFrontendUrlRoot() {
116 | const { data } = await this.client.query({
117 | query: FRONTEND_URL_ROOT,
118 | fetchPolicy: "cache-first",
119 | });
120 |
121 | return data?.frontendUrlRoot;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/language-server/engine/operations/frontendUrlRoot.ts:
--------------------------------------------------------------------------------
1 | import { TypedDocumentNode } from "@apollo/client/core";
2 | import gql from "graphql-tag";
3 | import type {
4 | FrontendUrlRootQuery,
5 | FrontendUrlRootQueryVariables,
6 | } from "src/language-server/graphqlTypes";
7 |
8 | export const FRONTEND_URL_ROOT: TypedDocumentNode<
9 | FrontendUrlRootQuery,
10 | FrontendUrlRootQueryVariables
11 | > = gql`
12 | query FrontendUrlRoot {
13 | frontendUrlRoot
14 | }
15 | `;
16 |
--------------------------------------------------------------------------------
/src/language-server/engine/operations/schemaTagsAndFieldStats.ts:
--------------------------------------------------------------------------------
1 | import { TypedDocumentNode } from "@apollo/client/core";
2 | import gql from "graphql-tag";
3 | import type {
4 | SchemaTagsAndFieldStatsQuery,
5 | SchemaTagsAndFieldStatsQueryVariables,
6 | } from "src/language-server/graphqlTypes";
7 |
8 | export const SCHEMA_TAGS_AND_FIELD_STATS: TypedDocumentNode<
9 | SchemaTagsAndFieldStatsQuery,
10 | SchemaTagsAndFieldStatsQueryVariables
11 | > = gql`
12 | query SchemaTagsAndFieldStats($id: ID!) {
13 | service(id: $id) {
14 | schemaTags {
15 | tag
16 | }
17 | stats(from: "-86400", to: "-0") {
18 | fieldLatencies {
19 | groupBy {
20 | parentType
21 | fieldName
22 | }
23 | metrics {
24 | fieldHistogram {
25 | durationMs(percentile: 0.95)
26 | }
27 | }
28 | }
29 | }
30 | }
31 | }
32 | `;
33 |
--------------------------------------------------------------------------------
/src/language-server/errors/__tests__/NoMissingClientDirectives.test.ts:
--------------------------------------------------------------------------------
1 | import { NoMissingClientDirectives } from "../validation";
2 | import { GraphQLClientProject } from "../../project/client";
3 | import { basename } from "path";
4 |
5 | import { vol } from "memfs";
6 | import { LoadingHandler } from "../../loadingHandler";
7 | import { ClientConfig, parseApolloConfig } from "../../config";
8 | import { URI } from "vscode-uri";
9 |
10 | const serviceSchema = /* GraphQL */ `
11 | type Query {
12 | me: User
13 | }
14 |
15 | type User {
16 | name: String
17 | friends: [User]
18 | }
19 | `;
20 | const clientSchema = /* GraphQL */ `
21 | extend type Query {
22 | isOnline: Boolean
23 | }
24 | extend type User {
25 | isLiked: Boolean
26 | localUser: User
27 | }
28 | `;
29 | const a = /* GraphQL */ `
30 | query a {
31 | isOnline
32 | me {
33 | name
34 | foo # added field missing in service schema to ensure it doesn't throw, see https://github.com/apollographql/vscode-graphql/pull/73
35 | localUser @client {
36 | friends {
37 | isLiked
38 | }
39 | }
40 | friends {
41 | name
42 | isLiked
43 | }
44 | }
45 | }
46 | `;
47 |
48 | const b = /* GraphQL */ `
49 | query b {
50 | me {
51 | ... {
52 | isLiked
53 | }
54 | ... @client {
55 | localUser {
56 | name
57 | }
58 | }
59 | }
60 | }
61 | `;
62 |
63 | const c = /* GraphQL */ `
64 | query c {
65 | me {
66 | ...isLiked
67 | }
68 | }
69 | fragment localUser on User @client {
70 | localUser {
71 | name
72 | }
73 | }
74 | fragment isLiked on User {
75 | isLiked
76 | ...localUser
77 | }
78 | `;
79 |
80 | const d = /* GraphQL */ `
81 | fragment isLiked on User {
82 | isLiked
83 | }
84 | query d {
85 | me {
86 | ...isLiked
87 | ...locaUser
88 | }
89 | }
90 | fragment localUser on User @client {
91 | localUser {
92 | name
93 | }
94 | }
95 | `;
96 |
97 | const e = /* GraphQL */ `
98 | fragment friends on User {
99 | friends {
100 | ...isLiked
101 | ... on User @client {
102 | localUser {
103 | name
104 | }
105 | }
106 | }
107 | }
108 | query e {
109 | isOnline @client
110 | me {
111 | ...friends
112 | }
113 | }
114 | fragment isLiked on User {
115 | isLiked
116 | }
117 | `;
118 |
119 | // TODO support inline fragment spreads
120 | const f = /* GraphQL */ `
121 | query f {
122 | me {
123 | ...isLiked @client
124 | }
125 | }
126 | fragment isLiked on User {
127 | isLiked
128 | }
129 | `;
130 |
131 | const rootURI = URI.file(process.cwd());
132 |
133 | const config = parseApolloConfig({
134 | client: {
135 | service: {
136 | name: "server",
137 | localSchemaFile: "./schema.graphql",
138 | },
139 | includes: ["./src/**.graphql"],
140 | excludes: ["./__tests__"],
141 | validationRules: [NoMissingClientDirectives],
142 | },
143 | engine: {},
144 | });
145 |
146 | class MockLoadingHandler implements LoadingHandler {
147 | handle(_message: string, value: Promise): Promise {
148 | return value;
149 | }
150 | handleSync(_message: string, value: () => T): T {
151 | return value();
152 | }
153 | showError(_message: string): void {}
154 | }
155 |
156 | jest.mock("fs");
157 |
158 | describe("client state", () => {
159 | beforeEach(() => {
160 | vol.fromJSON({
161 | "apollo.config.js": `module.exports = {
162 | client: {
163 | service: {
164 | localSchemaFile: './schema.graphql'
165 | }
166 | }
167 | }`,
168 | "schema.graphql": serviceSchema,
169 | "src/client-schema.graphql": clientSchema,
170 | "src/a.graphql": a,
171 | "src/b.graphql": b,
172 | "src/c.graphql": c,
173 | "src/d.graphql": d,
174 | "src/e.graphql": e,
175 | // "src/f.graphql": f,
176 | });
177 | });
178 | afterEach(jest.restoreAllMocks);
179 |
180 | it("should report validation errors for missing @client directives", async () => {
181 | const project = new GraphQLClientProject({
182 | config: config as ClientConfig,
183 | loadingHandler: new MockLoadingHandler(),
184 | configFolderURI: rootURI,
185 | clientIdentity: {
186 | name: "",
187 | version: "",
188 | referenceID: "",
189 | },
190 | });
191 |
192 | const errors = Object.create(null);
193 | project.onDiagnostics(({ diagnostics, uri }) => {
194 | const path = basename(URI.parse(uri).path);
195 | diagnostics.forEach(({ error }: any) => {
196 | if (!errors[path]) errors[path] = [];
197 | errors[path].push(error);
198 | });
199 | });
200 |
201 | await project.whenReady;
202 | await project.validate();
203 |
204 | expect(errors).toMatchInlineSnapshot(`
205 | Object {
206 | "a.graphql": Array [
207 | [GraphQLError: @client directive is missing on local field "isOnline"],
208 | [GraphQLError: @client directive is missing on local field "isLiked"],
209 | ],
210 | "b.graphql": Array [
211 | [GraphQLError: @client directive is missing on fragment around local fields "isLiked"],
212 | ],
213 | "c.graphql": Array [
214 | [GraphQLError: @client directive is missing on fragment "isLiked" around local fields "isLiked,localUser"],
215 | ],
216 | "d.graphql": Array [
217 | [GraphQLError: @client directive is missing on fragment "isLiked" around local fields "isLiked"],
218 | ],
219 | "e.graphql": Array [
220 | [GraphQLError: @client directive is missing on fragment "isLiked" around local fields "isLiked"],
221 | ],
222 | }
223 | `);
224 | });
225 | });
226 |
--------------------------------------------------------------------------------
/src/language-server/errors/logger.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLError } from "graphql";
2 | import path from "path";
3 |
4 | // ToolError is used for errors that are part of the expected flow
5 | // and for which a stack trace should not be printed
6 |
7 | export class ToolError extends Error {
8 | name: string = "ToolError";
9 |
10 | constructor(message: string) {
11 | super(message);
12 | this.message = message;
13 | }
14 | }
15 |
16 | const isRunningFromXcodeScript = process.env.XCODE_VERSION_ACTUAL;
17 |
18 | export function logError(error: Error) {
19 | if (error instanceof ToolError) {
20 | logErrorMessage(error.message);
21 | } else if (error instanceof GraphQLError) {
22 | const fileName = error.source && error.source.name;
23 | if (error.locations) {
24 | for (const location of error.locations) {
25 | logErrorMessage(error.message, fileName, location.line);
26 | }
27 | } else {
28 | logErrorMessage(error.message, fileName);
29 | }
30 | } else {
31 | console.error(error.stack);
32 | }
33 | }
34 |
35 | export function logErrorMessage(
36 | message: string,
37 | fileName?: string,
38 | lineNumber?: number,
39 | ) {
40 | if (isRunningFromXcodeScript) {
41 | if (fileName && lineNumber) {
42 | // Prefixing error output with file name, line and 'error: ',
43 | // so Xcode will associate it with the right file and display the error inline
44 | console.error(`${fileName}:${lineNumber}: error: ${message}`);
45 | } else {
46 | // Prefixing error output with 'error: ', so Xcode will display it as an error
47 | console.error(`error: ${message}`);
48 | }
49 | } else {
50 | if (fileName) {
51 | const truncatedFileName =
52 | "/" + fileName.split(path.sep).slice(-4).join(path.sep);
53 | console.error(`...${truncatedFileName}: ${message}`);
54 | } else {
55 | console.error(`error: ${message}`);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/language-server/fileSet.ts:
--------------------------------------------------------------------------------
1 | import { minimatch } from "minimatch";
2 | import { globSync } from "glob";
3 | import { invariant } from "../tools";
4 | import { URI } from "vscode-uri";
5 | import { normalizeURI, withUnixSeparator } from "./utilities";
6 | import { resolve } from "path";
7 |
8 | export class FileSet {
9 | private rootURI: URI;
10 | private includes: string[];
11 | private excludes: string[];
12 |
13 | constructor({
14 | rootURI,
15 | includes,
16 | excludes,
17 | }: {
18 | rootURI: URI;
19 | includes: string[];
20 | excludes: string[];
21 | }) {
22 | invariant(rootURI, `Must provide "rootURI".`);
23 | invariant(includes, `Must provide "includes".`);
24 | invariant(excludes, `Must provide "excludes".`);
25 |
26 | this.rootURI = rootURI;
27 | this.includes = includes;
28 | this.excludes = excludes;
29 | }
30 |
31 | includesFile(filePath: string): boolean {
32 | const normalizedFilePath = normalizeURI(filePath);
33 |
34 | return (
35 | this.includes.some((include) => {
36 | return minimatch(
37 | normalizedFilePath,
38 | withUnixSeparator(resolve(this.rootURI.fsPath, include)),
39 | );
40 | }) &&
41 | !this.excludes.some((exclude) => {
42 | return minimatch(
43 | normalizedFilePath,
44 | withUnixSeparator(resolve(this.rootURI.fsPath, exclude)),
45 | );
46 | })
47 | );
48 | }
49 |
50 | allFiles(): string[] {
51 | const joinedIncludes =
52 | this.includes.length == 1
53 | ? this.includes[0]
54 | : // since glob.sync takes a single pattern, but we allow an array of `includes`, we can join all the
55 | // `includes` globs into a single pattern and pass to glob.sync. The `ignore` option does, however, allow
56 | // an array of globs to ignore, so we can pass it in directly
57 | `{${this.includes.join(",")}}`;
58 |
59 | return globSync(joinedIncludes, {
60 | cwd: this.rootURI.fsPath,
61 | absolute: true,
62 | ignore: this.excludes,
63 | }).map(normalizeURI);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/language-server/format.ts:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 |
3 | export function formatMS(
4 | ms: number,
5 | d: number,
6 | allowMicros = false,
7 | allowNanos = true,
8 | ) {
9 | if (ms === 0 || ms === null) return "0";
10 | const bounds = [
11 | moment.duration(1, "hour").asMilliseconds(),
12 | moment.duration(1, "minute").asMilliseconds(),
13 | moment.duration(1, "second").asMilliseconds(),
14 | 1,
15 | 0.001,
16 | 0.000001,
17 | ];
18 | const units = ["hr", "min", "s", "ms", "μs", "ns"];
19 |
20 | const makeSmallNumbersNice = (f: number) => {
21 | if (f >= 100) return f.toFixed(0);
22 | if (f >= 10) return f.toFixed(1);
23 | if (f === 0) return "0";
24 | return f.toFixed(2);
25 | };
26 |
27 | const bound = bounds.find((b) => b <= ms) || bounds[bounds.length - 1];
28 | const boundIndex = bounds.indexOf(bound);
29 | const unit = boundIndex >= 0 ? units[boundIndex] : "";
30 |
31 | if ((unit === "μs" || unit === "ns") && !allowMicros) {
32 | return "< 1ms";
33 | }
34 | if (unit === "ns" && !allowNanos) {
35 | return "< 1µs";
36 | }
37 | const value =
38 | typeof d !== "undefined"
39 | ? (ms / bound).toFixed(d)
40 | : makeSmallNumbersNice(ms / bound);
41 |
42 | // if something is rounded to 1000 and not reduced this will catch and reduce it
43 | if ((value === "1000" || value === "1000.0") && boundIndex >= 1) {
44 | return `1${units[boundIndex - 1]}`;
45 | }
46 |
47 | return `${value}${unit}`;
48 | }
49 |
--------------------------------------------------------------------------------
/src/language-server/index.ts:
--------------------------------------------------------------------------------
1 | // Exports for consuming APIs
2 |
3 | export { getValidationErrors } from "./errors/validation";
4 | export { ToolError } from "./errors/logger";
5 | export { LoadingHandler } from "./loadingHandler";
6 |
7 | // projects
8 | export { GraphQLProject } from "./project/base";
9 | export { isClientProject, GraphQLClientProject } from "./project/client";
10 |
11 | // GraphQLSchemaProvider
12 | export {
13 | GraphQLSchemaProvider,
14 | schemaProviderFromConfig,
15 | } from "./providers/schema";
16 |
17 | // Engine
18 | export * from "./engine";
19 |
20 | // Config
21 | export * from "./config";
22 |
23 | // Generated types
24 | import * as graphqlTypes from "./graphqlTypes";
25 | export { graphqlTypes };
26 |
27 | // debug logger
28 | export { Debug } from "./utilities";
29 |
--------------------------------------------------------------------------------
/src/language-server/loadingHandler.ts:
--------------------------------------------------------------------------------
1 | import { LanguageServerNotifications as Notifications } from "../messages";
2 | import { Connection, NotificationType } from "vscode-languageserver/node";
3 |
4 | // XXX I think we want to combine this into an interface
5 | // with the errors tooling as well
6 |
7 | export interface LoadingHandler {
8 | handle(message: string, value: Promise): Promise;
9 | handleSync(message: string, value: () => T): T;
10 | showError(message: string): void;
11 | }
12 |
13 | export class LanguageServerLoadingHandler implements LoadingHandler {
14 | constructor(private connection: Connection) {}
15 | private latestLoadingToken = 0;
16 | async handle(message: string, value: Promise): Promise {
17 | const token = this.latestLoadingToken;
18 | this.latestLoadingToken += 1;
19 | this.connection.sendNotification(Notifications.Loading, { message, token });
20 | try {
21 | const ret = await value;
22 | this.connection.sendNotification(Notifications.LoadingComplete, token);
23 | return ret;
24 | } catch (e) {
25 | this.connection.sendNotification(Notifications.LoadingComplete, token);
26 | this.showError(`Error in "${message}": ${e}`);
27 | throw e;
28 | }
29 | }
30 | handleSync(message: string, value: () => T): T {
31 | const token = this.latestLoadingToken;
32 | this.latestLoadingToken += 1;
33 | this.connection.sendNotification(Notifications.Loading, { message, token });
34 | try {
35 | const ret = value();
36 | this.connection.sendNotification(Notifications.LoadingComplete, token);
37 | return ret;
38 | } catch (e) {
39 | Notifications.LoadingComplete,
40 | this.showError(`Error in "${message}": ${e}`);
41 | throw e;
42 | }
43 | }
44 | showError(message: string) {
45 | this.connection.window.showErrorMessage(message);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/language-server/project/base.ts:
--------------------------------------------------------------------------------
1 | import { URI } from "vscode-uri";
2 |
3 | import { GraphQLSchema } from "graphql";
4 |
5 | import {
6 | NotificationHandler,
7 | PublishDiagnosticsParams,
8 | CancellationToken,
9 | SymbolInformation,
10 | Connection,
11 | ServerRequestHandler,
12 | TextDocumentChangeEvent,
13 | StarRequestHandler,
14 | StarNotificationHandler,
15 | ServerCapabilities,
16 | } from "vscode-languageserver/node";
17 | import { TextDocument } from "vscode-languageserver-textdocument";
18 |
19 | import type { LoadingHandler } from "../loadingHandler";
20 | import { FileSet } from "../fileSet";
21 | import {
22 | ApolloConfig,
23 | ClientConfig,
24 | envFileNames,
25 | RoverConfig,
26 | supportedConfigFileNames,
27 | } from "../config";
28 | import type { ProjectStats } from "../../messages";
29 |
30 | export type DocumentUri = string;
31 |
32 | export interface GraphQLProjectConfig {
33 | config: ClientConfig | RoverConfig;
34 | configFolderURI: URI;
35 | loadingHandler: LoadingHandler;
36 | }
37 |
38 | type ConnectionHandler = {
39 | [K in keyof Connection as K extends `on${string}`
40 | ? K
41 | : never]: Connection[K] extends (
42 | params: ServerRequestHandler & infer P,
43 | token: CancellationToken,
44 | ) => any
45 | ? P
46 | : never;
47 | };
48 |
49 | export abstract class GraphQLProject {
50 | protected _onDiagnostics?: NotificationHandler;
51 |
52 | private _isReady: boolean;
53 | private readyPromise: Promise;
54 | public config: ApolloConfig;
55 | protected schema?: GraphQLSchema;
56 | protected rootURI: URI;
57 | protected loadingHandler: LoadingHandler;
58 |
59 | protected lastLoadDate?: number;
60 |
61 | private configFileSet: FileSet;
62 |
63 | constructor({
64 | config,
65 | configFolderURI,
66 | loadingHandler,
67 | }: GraphQLProjectConfig) {
68 | this.config = config;
69 | this.loadingHandler = loadingHandler;
70 | // the URI of the folder _containing_ the apollo.config.js is the true project's root.
71 | // if a config doesn't have a uri associated, we can assume the `rootURI` is the project's root.
72 | this.rootURI = config.configDirURI || configFolderURI;
73 |
74 | this.configFileSet = new FileSet({
75 | rootURI: this.rootURI,
76 | includes: supportedConfigFileNames.concat(envFileNames),
77 | excludes: [],
78 | });
79 |
80 | this._isReady = false;
81 | this.readyPromise = Promise.resolve()
82 | .then(
83 | // FIXME: Instead of `Promise.all`, we should catch individual promise rejections
84 | // so we can show multiple errors.
85 | () => Promise.all(this.initialize()),
86 | )
87 | .then(() => {
88 | this._isReady = true;
89 | })
90 | .catch((error) => {
91 | console.error(error);
92 | this.loadingHandler.showError(
93 | `Error initializing Apollo GraphQL project "${this.displayName}": ${error}`,
94 | );
95 | });
96 | }
97 |
98 | abstract get displayName(): string;
99 |
100 | abstract initialize(): Promise[];
101 |
102 | abstract getProjectStats(): ProjectStats;
103 |
104 | get isReady(): boolean {
105 | return this._isReady;
106 | }
107 |
108 | get whenReady(): Promise {
109 | return this.readyPromise;
110 | }
111 |
112 | public updateConfig(config: ApolloConfig) {
113 | this.config = config;
114 | return this.initialize();
115 | }
116 |
117 | onDiagnostics(handler: NotificationHandler) {
118 | this._onDiagnostics = handler;
119 | }
120 |
121 | abstract includesFile(uri: DocumentUri, languageId?: string): boolean;
122 | isConfiguredBy(uri: DocumentUri): boolean {
123 | return this.configFileSet.includesFile(uri);
124 | }
125 |
126 | abstract onDidChangeWatchedFiles: ConnectionHandler["onDidChangeWatchedFiles"];
127 | onDidOpen?: (event: TextDocumentChangeEvent) => void;
128 | onDidClose?: (event: TextDocumentChangeEvent) => void;
129 | abstract documentDidChange(document: TextDocument): void;
130 | abstract clearAllDiagnostics(): void;
131 |
132 | onCompletion?: ConnectionHandler["onCompletion"];
133 | onHover?: ConnectionHandler["onHover"];
134 | onDefinition?: ConnectionHandler["onDefinition"];
135 | onReferences?: ConnectionHandler["onReferences"];
136 | onDocumentSymbol?: ConnectionHandler["onDocumentSymbol"];
137 | onCodeLens?: ConnectionHandler["onCodeLens"];
138 | onCodeAction?: ConnectionHandler["onCodeAction"];
139 |
140 | onUnhandledRequest?: StarRequestHandler;
141 | onUnhandledNotification?: (
142 | connection: Connection,
143 | ...rest: Parameters
144 | ) => ReturnType;
145 |
146 | dispose?(): void;
147 |
148 | provideSymbol?(
149 | query: string,
150 | token: CancellationToken,
151 | ): Promise;
152 |
153 | onVSCodeConnectionInitialized?(connection: Connection): void;
154 | validate?(): void;
155 | }
156 |
--------------------------------------------------------------------------------
/src/language-server/project/defaultClientSchema.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLDocument } from "../document";
2 | import { Source } from "graphql";
3 |
4 | export const apolloClientSchema = `#graphql
5 | """
6 | Direct the client to resolve this field locally, either from the cache or local resolvers.
7 | """
8 | directive @client(
9 | """
10 | When true, the client will never use the cache for this value. See
11 | https://www.apollographql.com/docs/react/local-state/local-resolvers/#forcing-resolvers-with-clientalways-true
12 | """
13 | always: Boolean
14 | ) on FIELD | FRAGMENT_DEFINITION | INLINE_FRAGMENT
15 |
16 | """
17 | Export this locally resolved field as a variable to be used in the remainder of this query. See
18 | https://www.apollographql.com/docs/react/local-state/local-resolvers/#using-client-fields-as-variables
19 | """
20 | directive @export(
21 | """
22 | The variable name to export this field as.
23 | """
24 | as: String!
25 | ) on FIELD
26 |
27 | """
28 | Specify a custom store key for this result. See
29 | https://www.apollographql.com/docs/react/caching/advanced-topics/#the-connection-directive
30 | """
31 | directive @connection(
32 | """
33 | Specify the store key.
34 | """
35 | key: String!
36 | """
37 | An array of query argument names to include in the generated custom store key.
38 | """
39 | filter: [String!]
40 | ) on FIELD
41 |
42 | """
43 | The @nonreactive directive can be used to mark query fields or fragment spreads and is used to indicate that changes to the data contained within the subtrees marked @nonreactive should not trigger rerendering.
44 | This allows parent components to fetch data to be rendered by their children without rerendering themselves when the data corresponding with fields marked as @nonreactive change.
45 | https://www.apollographql.com/docs/react/data/directives#nonreactive
46 | """
47 | directive @nonreactive on FIELD
48 |
49 | """
50 | This directive enables your queries to receive data for specific fields incrementally, instead of receiving all field data at the same time.
51 | This is helpful whenever some fields in a query take much longer to resolve than others.
52 | https://www.apollographql.com/docs/react/data/directives#defer
53 | """
54 | directive @defer(
55 | """
56 | When true fragment may be deferred, if omitted defaults to true.
57 | """
58 | if: Boolean
59 | """
60 | A unique label across all @defer and @stream directives in an operation.
61 | This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragment.
62 | If provided, the GraphQL Server must add it to the payload.
63 | """
64 | label: String
65 | ) on FRAGMENT_SPREAD | INLINE_FRAGMENT
66 | `;
67 |
68 | export const apolloClientSchemaDocument = new GraphQLDocument(
69 | new Source(apolloClientSchema),
70 | );
71 |
--------------------------------------------------------------------------------
/src/language-server/providers/schema/__tests__/file.ts:
--------------------------------------------------------------------------------
1 | import { FileSchemaProvider } from "../file";
2 | import * as path from "path";
3 | import * as fs from "fs";
4 | import { Debug } from "../../../utilities";
5 | import { URI } from "vscode-uri";
6 |
7 | const makeNestedDir = (dir: string) => {
8 | if (fs.existsSync(dir)) return;
9 |
10 | try {
11 | fs.mkdirSync(dir);
12 | } catch (err: any) {
13 | if (err.code == "ENOENT") {
14 | makeNestedDir(path.dirname(dir)); //create parent dir
15 | makeNestedDir(dir); //create dir
16 | }
17 | }
18 | };
19 |
20 | const deleteFolderRecursive = (path: string) => {
21 | // don't delete files on windows -- will get a resource locked error
22 | if (require("os").type().includes("Windows")) {
23 | return;
24 | }
25 |
26 | if (fs.existsSync(path)) {
27 | fs.readdirSync(path).forEach(function (file, index) {
28 | var curPath = path + "/" + file;
29 | if (fs.lstatSync(curPath).isDirectory()) {
30 | // recurse
31 | deleteFolderRecursive(curPath);
32 | } else {
33 | // delete file
34 | fs.unlinkSync(curPath);
35 | }
36 | });
37 | fs.rmdirSync(path);
38 | }
39 | };
40 |
41 | const writeFilesToDir = (dir: string, files: Record) => {
42 | Object.keys(files).forEach((key) => {
43 | if (key.includes("/")) makeNestedDir(path.dirname(key));
44 | fs.writeFileSync(`${dir}/${key}`, files[key]);
45 | });
46 | };
47 |
48 | describe("FileSchemaProvider", () => {
49 | let dir: string;
50 | let dirPath: string;
51 |
52 | // set up a temp dir
53 | beforeEach(() => {
54 | dir = fs.mkdtempSync("__tmp__");
55 | dirPath = `${process.cwd()}/${dir}`;
56 | });
57 |
58 | // clean up our temp dir
59 | afterEach(() => {
60 | if (dir) {
61 | deleteFolderRecursive(dir);
62 | }
63 | });
64 |
65 | describe("resolveFederatedServiceSDL", () => {
66 | it("finds and loads sdl from graphql file for a federated service", async () => {
67 | writeFilesToDir(dir, {
68 | "schema.graphql": `
69 | extend type Query {
70 | myProduct: Product
71 | }
72 |
73 | type Product @key(fields: "id") {
74 | id: ID
75 | sku: ID
76 | name: String
77 | }
78 | `,
79 | });
80 |
81 | const provider = new FileSchemaProvider(
82 | {
83 | path: "./schema.graphql",
84 | },
85 | URI.from({ scheme: "file", path: dirPath }),
86 | );
87 | const sdl = await provider.resolveFederatedServiceSDL();
88 | expect(sdl).toMatchInlineSnapshot;
89 | });
90 |
91 | it("finds and loads sdl from multiple graphql files for a federated service", async () => {
92 | writeFilesToDir(dir, {
93 | "schema.graphql": `
94 | extend type Query {
95 | myProduct: Product
96 | }
97 |
98 | type Product @key(fields: "id") {
99 | id: ID
100 | sku: ID
101 | name: String
102 | }`,
103 | "schema2.graphql": `
104 | extend type Product {
105 | weight: Float
106 | }`,
107 | });
108 |
109 | const provider = new FileSchemaProvider(
110 | {
111 | paths: ["schema.graphql", "schema2.graphql"],
112 | },
113 | URI.from({ scheme: "file", path: dirPath }),
114 | );
115 | const sdl = await provider.resolveFederatedServiceSDL();
116 | expect(sdl).toMatchInlineSnapshot(`
117 | "directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
118 |
119 | directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
120 |
121 | directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
122 |
123 | directive @external(reason: String) on OBJECT | FIELD_DEFINITION
124 |
125 | directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
126 |
127 | directive @extends on OBJECT | INTERFACE
128 |
129 | type Query {
130 | _entities(representations: [_Any!]!): [_Entity]!
131 | _service: _Service!
132 | }
133 |
134 | extend type Query {
135 | myProduct: Product
136 | }
137 |
138 | type Product
139 | @key(fields: \\"id\\")
140 | {
141 | id: ID
142 | sku: ID
143 | name: String
144 | }
145 |
146 | extend type Product {
147 | weight: Float
148 | }
149 |
150 | scalar _FieldSet
151 |
152 | scalar _Any
153 |
154 | type _Service {
155 | sdl: String
156 | }
157 |
158 | union _Entity = Product"
159 | `);
160 | });
161 |
162 | it("errors when sdl file is not a graphql file", async () => {
163 | const toWrite = `
164 | module.exports = \`
165 | extend type Query {
166 | myProduct: Product
167 | }
168 |
169 | type Product @key(fields: "id") {
170 | id: ID
171 | sku: ID
172 | name: string
173 | }\`
174 | `;
175 | writeFilesToDir(dir, {
176 | "schema.js": toWrite,
177 | });
178 |
179 | // noop -- just spy on and silence the error
180 | const errorSpy = jest.spyOn(Debug, "error");
181 | errorSpy.mockImplementation(() => {});
182 |
183 | const provider = new FileSchemaProvider(
184 | { path: "./schema.js" },
185 | URI.from({ scheme: "file", path: dirPath }),
186 | );
187 | const sdl = await provider.resolveFederatedServiceSDL();
188 | expect(errorSpy).toBeCalledTimes(2);
189 | });
190 | });
191 | });
192 |
--------------------------------------------------------------------------------
/src/language-server/providers/schema/base.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLSchema } from "graphql";
2 | import { NotificationHandler } from "vscode-languageserver/node";
3 |
4 | export interface SchemaResolveConfig {
5 | tag?: string;
6 | force?: boolean;
7 | }
8 | export type SchemaChangeUnsubscribeHandler = () => void;
9 | export interface GraphQLSchemaProvider {
10 | resolveSchema(config?: SchemaResolveConfig): Promise;
11 | onSchemaChange(
12 | handler: NotificationHandler,
13 | ): SchemaChangeUnsubscribeHandler;
14 | resolveFederatedServiceSDL(): Promise;
15 | }
16 |
--------------------------------------------------------------------------------
/src/language-server/providers/schema/endpoint.ts:
--------------------------------------------------------------------------------
1 | // IntrospectionSchemaProvider (http => IntrospectionResult => schema)
2 | import { NotificationHandler } from "vscode-languageserver/node";
3 |
4 | import { execute as linkExecute } from "@apollo/client/link/core";
5 | import { toPromise } from "@apollo/client/link/utils";
6 | import { createHttpLink, HttpOptions } from "@apollo/client/link/http";
7 | import {
8 | GraphQLSchema,
9 | buildClientSchema,
10 | getIntrospectionQuery,
11 | ExecutionResult,
12 | IntrospectionQuery,
13 | parse,
14 | } from "graphql";
15 | import { Agent as HTTPSAgent } from "https";
16 | import { RemoteServiceConfig } from "../../config";
17 | import { GraphQLSchemaProvider, SchemaChangeUnsubscribeHandler } from "./base";
18 | import { Debug } from "../../utilities";
19 | import { isString } from "util";
20 | import { fetch as undiciFetch, Agent } from "undici";
21 |
22 | const skipSSLValidationFetchOptions = {
23 | // see https://github.com/nodejs/undici/issues/1489#issuecomment-1543856261
24 | dispatcher: new Agent({
25 | connect: {
26 | rejectUnauthorized: false,
27 | },
28 | }),
29 | } satisfies import("undici").RequestInit;
30 | export class EndpointSchemaProvider implements GraphQLSchemaProvider {
31 | private schema?: GraphQLSchema;
32 | private federatedServiceSDL?: string;
33 |
34 | constructor(private config: Exclude) {}
35 | async resolveSchema() {
36 | if (this.schema) return this.schema;
37 | const { skipSSLValidation, url, headers } = this.config;
38 | const options: HttpOptions = {
39 | uri: url,
40 | fetch: undiciFetch as typeof fetch,
41 | };
42 |
43 | if (url.startsWith("https:") && skipSSLValidation) {
44 | options.fetchOptions = skipSSLValidationFetchOptions;
45 | }
46 |
47 | const { data, errors } = (await toPromise(
48 | linkExecute(createHttpLink(options), {
49 | query: parse(getIntrospectionQuery()),
50 | context: { headers },
51 | }),
52 | ).catch((e) => {
53 | // html response from introspection
54 | if (isString(e.message) && e.message.includes("token <")) {
55 | throw new Error(
56 | "Apollo tried to introspect a running GraphQL service at " +
57 | url +
58 | "\nIt expected a JSON schema introspection result, but got an HTML response instead." +
59 | "\nYou may need to add headers to your request or adjust your endpoint url.\n" +
60 | "-----------------------------\n" +
61 | "For more information, please refer to: https://go.apollo.dev/t/config \n\n" +
62 | "The following error occurred:\n-----------------------------\n" +
63 | e.message,
64 | );
65 | }
66 |
67 | // 404 with a non-default url
68 | if (isString(e.message) && e.message.includes("ECONNREFUSED")) {
69 | throw new Error(
70 | "Failed to connect to a running GraphQL endpoint at " +
71 | url +
72 | "\nThis may be because you didn't start your service or the endpoint URL is incorrect.",
73 | );
74 | }
75 | throw new Error(e);
76 | })) as ExecutionResult;
77 |
78 | if (errors && errors.length) {
79 | // XXX better error handling of GraphQL errors
80 | throw new Error(errors.map(({ message }: Error) => message).join("\n"));
81 | }
82 |
83 | if (!data) {
84 | throw new Error("No data received from server introspection.");
85 | }
86 |
87 | this.schema = buildClientSchema(data);
88 | return this.schema;
89 | }
90 |
91 | onSchemaChange(
92 | _handler: NotificationHandler,
93 | ): SchemaChangeUnsubscribeHandler {
94 | throw new Error("Polling of endpoint not implemented yet");
95 | return () => {};
96 | }
97 |
98 | async resolveFederatedServiceSDL() {
99 | if (this.federatedServiceSDL) return this.federatedServiceSDL;
100 |
101 | const { skipSSLValidation, url, headers } = this.config;
102 | const options: HttpOptions = {
103 | uri: url,
104 | fetch: undiciFetch as typeof fetch,
105 | };
106 | if (url.startsWith("https:") && skipSSLValidation) {
107 | options.fetchOptions = skipSSLValidationFetchOptions;
108 | }
109 |
110 | const getFederationInfoQuery = `
111 | query getFederationInfo {
112 | _service {
113 | sdl
114 | }
115 | }
116 | `;
117 |
118 | const { data, errors } = (await toPromise(
119 | linkExecute(createHttpLink(options), {
120 | query: parse(getFederationInfoQuery),
121 | context: { headers },
122 | }),
123 | )) as ExecutionResult<{ _service: { sdl: string } }>;
124 |
125 | if (errors && errors.length) {
126 | return Debug.error(
127 | errors.map(({ message }: Error) => message).join("\n"),
128 | );
129 | }
130 |
131 | if (!data || !data._service) {
132 | return Debug.error(
133 | "No data received from server when querying for _service.",
134 | );
135 | }
136 |
137 | this.federatedServiceSDL = data._service.sdl;
138 | return data._service.sdl;
139 | }
140 |
141 | // public async isFederatedSchema() {
142 | // const schema = this.schema || (await this.resolveSchema());
143 | // return false;
144 | // }
145 | }
146 |
--------------------------------------------------------------------------------
/src/language-server/providers/schema/engine.ts:
--------------------------------------------------------------------------------
1 | // EngineSchemaProvider (engine schema reg => schema)
2 | import { NotificationHandler } from "vscode-languageserver/node";
3 | import gql from "graphql-tag";
4 | import { GraphQLSchema, IntrospectionQuery, buildClientSchema } from "graphql";
5 | import { ApolloEngineClient, ClientIdentity } from "../../engine";
6 | import { ClientConfig, keyEnvVar } from "../../config";
7 | import {
8 | GraphQLSchemaProvider,
9 | SchemaChangeUnsubscribeHandler,
10 | SchemaResolveConfig,
11 | } from "./base";
12 |
13 | import {
14 | GetSchemaByTagQuery,
15 | GetSchemaByTagQueryVariables,
16 | } from "../../graphqlTypes";
17 | import { Debug } from "../../utilities";
18 | import { TypedDocumentNode } from "@apollo/client/core";
19 |
20 | export class EngineSchemaProvider implements GraphQLSchemaProvider {
21 | private schema?: GraphQLSchema;
22 | private client?: ApolloEngineClient;
23 |
24 | constructor(
25 | private config: ClientConfig,
26 | private clientIdentity: ClientIdentity,
27 | ) {}
28 |
29 | async resolveSchema(override: SchemaResolveConfig) {
30 | if (this.schema && (!override || !override.force)) return this.schema;
31 | const { engine, client } = this.config;
32 |
33 | if (!this.config.graph) {
34 | throw new Error(
35 | `No graph ID found for client. Please specify a graph ID via the config or the --graph flag`,
36 | );
37 | }
38 |
39 | // create engine client
40 | if (!this.client) {
41 | if (!engine.apiKey) {
42 | throw new Error(
43 | `No API key found. Please set ${keyEnvVar} or use --key`,
44 | );
45 | }
46 | this.client = new ApolloEngineClient(
47 | engine.apiKey,
48 | engine.endpoint,
49 | this.clientIdentity,
50 | );
51 | }
52 |
53 | const { data, errors } = await this.client.query({
54 | query: SCHEMA_QUERY,
55 | variables: {
56 | id: this.config.graph,
57 | tag: override && override.tag ? override.tag : this.config.variant,
58 | },
59 | fetchPolicy: "no-cache",
60 | });
61 | if (errors) {
62 | // XXX better error handling of GraphQL errors
63 | throw new Error(errors.map(({ message }) => message).join("\n"));
64 | }
65 |
66 | if (!(data && data.service && data.service.__typename === "Service")) {
67 | throw new Error(
68 | `Unable to get schema from the Apollo registry for graph ${this.config.graph}`,
69 | );
70 | }
71 |
72 | this.schema = buildClientSchema(
73 | data.service.schema as unknown as IntrospectionQuery,
74 | );
75 | return this.schema;
76 | }
77 |
78 | onSchemaChange(
79 | _handler: NotificationHandler,
80 | ): SchemaChangeUnsubscribeHandler {
81 | throw new Error("Polling of Apollo not implemented yet");
82 | return () => {};
83 | }
84 |
85 | async resolveFederatedServiceSDL() {
86 | Debug.error(
87 | "Cannot resolve a federated service's SDL from Apollo. Use an endpoint or a file instead",
88 | );
89 | return;
90 | }
91 | }
92 |
93 | export const SCHEMA_QUERY: TypedDocumentNode<
94 | GetSchemaByTagQuery,
95 | GetSchemaByTagQueryVariables
96 | > = gql`
97 | query GetSchemaByTag($tag: String!, $id: ID!) {
98 | service(id: $id) {
99 | ... on Service {
100 | __typename
101 | schema(tag: $tag) {
102 | hash
103 | __schema: introspection {
104 | queryType {
105 | name
106 | }
107 | mutationType {
108 | name
109 | }
110 | subscriptionType {
111 | name
112 | }
113 | types(filter: { includeBuiltInTypes: true }) {
114 | ...IntrospectionFullType
115 | }
116 | directives {
117 | name
118 | description
119 | locations
120 | args {
121 | ...IntrospectionInputValue
122 | }
123 | }
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
130 | fragment IntrospectionFullType on IntrospectionType {
131 | kind
132 | name
133 | description
134 | fields {
135 | name
136 | description
137 | args {
138 | ...IntrospectionInputValue
139 | }
140 | type {
141 | ...IntrospectionTypeRef
142 | }
143 | isDeprecated
144 | deprecationReason
145 | }
146 | inputFields {
147 | ...IntrospectionInputValue
148 | }
149 | interfaces {
150 | ...IntrospectionTypeRef
151 | }
152 | enumValues(includeDeprecated: true) {
153 | name
154 | description
155 | isDeprecated
156 | deprecationReason
157 | }
158 | possibleTypes {
159 | ...IntrospectionTypeRef
160 | }
161 | }
162 |
163 | fragment IntrospectionInputValue on IntrospectionInputValue {
164 | name
165 | description
166 | type {
167 | ...IntrospectionTypeRef
168 | }
169 | defaultValue
170 | }
171 |
172 | fragment IntrospectionTypeRef on IntrospectionType {
173 | kind
174 | name
175 | ofType {
176 | kind
177 | name
178 | ofType {
179 | kind
180 | name
181 | ofType {
182 | kind
183 | name
184 | ofType {
185 | kind
186 | name
187 | ofType {
188 | kind
189 | name
190 | ofType {
191 | kind
192 | name
193 | ofType {
194 | kind
195 | name
196 | }
197 | }
198 | }
199 | }
200 | }
201 | }
202 | }
203 | }
204 | `;
205 |
--------------------------------------------------------------------------------
/src/language-server/providers/schema/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLSchemaProvider,
3 | SchemaChangeUnsubscribeHandler,
4 | SchemaResolveConfig,
5 | } from "./base";
6 | import {
7 | ApolloConfig,
8 | isClientConfig,
9 | isLocalServiceConfig,
10 | ClientConfig,
11 | } from "../../config";
12 |
13 | import { EndpointSchemaProvider } from "./endpoint";
14 | import { EngineSchemaProvider } from "./engine";
15 | import { FileSchemaProvider } from "./file";
16 | import { ClientIdentity } from "../../engine";
17 |
18 | export {
19 | GraphQLSchemaProvider,
20 | SchemaChangeUnsubscribeHandler,
21 | SchemaResolveConfig,
22 | };
23 |
24 | export function schemaProviderFromConfig(
25 | config: ApolloConfig,
26 | clientIdentity: ClientIdentity, // engine provider needs this
27 | ): GraphQLSchemaProvider {
28 | if (isClientConfig(config)) {
29 | if (typeof config.client.service === "string") {
30 | return new EngineSchemaProvider(config, clientIdentity);
31 | }
32 |
33 | if (config.client.service) {
34 | if (isLocalServiceConfig(config.client.service)) {
35 | const isListOfSchemaFiles = Array.isArray(
36 | config.client.service.localSchemaFile,
37 | );
38 | return new FileSchemaProvider(
39 | isListOfSchemaFiles
40 | ? { paths: config.client.service.localSchemaFile as string[] }
41 | : {
42 | path: config.client.service.localSchemaFile as string,
43 | },
44 | config.configDirURI,
45 | );
46 | }
47 |
48 | return new EndpointSchemaProvider(config.client.service);
49 | }
50 | }
51 |
52 | if (config.graph && config.engine) {
53 | return new EngineSchemaProvider(config as ClientConfig, clientIdentity);
54 | }
55 |
56 | throw new Error(
57 | "No schema provider was created, because the project type was unable to be resolved from your config. Please add either a client or service config. For more information, please refer to https://go.apollo.dev/t/config",
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/language-server/typings/graphql.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ASTNode,
3 | TypeSystemDefinitionNode,
4 | TypeSystemExtensionNode,
5 | FragmentDefinitionNode,
6 | OperationDefinitionNode,
7 | } from "graphql";
8 |
9 | // FIXME: We should add proper type guards for these predicate functions
10 | // to `@types/graphql`.
11 | declare module "graphql/language/predicates" {
12 | function isExecutableDefinitionNode(
13 | node: ASTNode,
14 | ): node is OperationDefinitionNode | FragmentDefinitionNode;
15 | function isTypeSystemDefinitionNode(
16 | node: ASTNode,
17 | ): node is TypeSystemDefinitionNode;
18 | function isTypeSystemExtensionNode(
19 | node: ASTNode,
20 | ): node is TypeSystemExtensionNode;
21 | }
22 |
23 | declare module "graphql/validation/validate" {
24 | interface ValidationContext {
25 | _fragments: { [fragmentName: string]: FragmentDefinitionNode };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/language-server/utilities/__tests__/source.test.ts:
--------------------------------------------------------------------------------
1 | import { extractGraphQLSources } from "../../document";
2 | import { TextDocument } from "vscode-languageserver-textdocument";
3 | import {
4 | findContainedSourceAndPosition,
5 | positionFromPositionInContainingDocument,
6 | positionInContainingDocument,
7 | } from "../source";
8 | import { handleFilePartUpdates } from "../../project/rover/DocumentSynchronization";
9 |
10 | const testText = `import gql from "graphql-tag";
11 |
12 | const foo = 1
13 |
14 | gql\`
15 | query Test {
16 | droid(id: "2000") {
17 | name
18 | }
19 |
20 | }\`;
21 |
22 | const verylonglala = gql\`type Foo { baaaaaar: String }\`
23 | `;
24 | describe("positionFromPositionInContainingDocument", () => {
25 | const sources = extractGraphQLSources(
26 | TextDocument.create("uri", "javascript", 1, testText),
27 | "gql",
28 | )!;
29 |
30 | test("should return the correct position inside a document", () => {
31 | expect(
32 | positionFromPositionInContainingDocument(sources[0], {
33 | line: 5,
34 | character: 3,
35 | }),
36 | ).toEqual({ line: 1, character: 3 });
37 | });
38 |
39 | test("should return the correct position on the first line of a document", () => {
40 | expect(
41 | positionFromPositionInContainingDocument(sources[0], {
42 | line: 4,
43 | character: 4,
44 | }),
45 | ).toEqual({ line: 0, character: 0 });
46 | });
47 |
48 | test("should return the correct position on a single line document", () => {
49 | expect(
50 | positionFromPositionInContainingDocument(sources[1], {
51 | line: 12,
52 | character: 46,
53 | }),
54 | ).toEqual({ line: 0, character: 21 });
55 | });
56 | });
57 |
58 | describe("findContainedSourceAndPosition", () => {
59 | const parts = handleFilePartUpdates(
60 | extractGraphQLSources(
61 | TextDocument.create("uri", "javascript", 1, testText),
62 | "gql",
63 | )!,
64 | [],
65 | );
66 |
67 | test("should return the correct position inside a document", () => {
68 | expect(
69 | findContainedSourceAndPosition(parts, {
70 | line: 5,
71 | character: 3,
72 | }),
73 | ).toEqual({ ...parts[0], position: { line: 1, character: 3 } });
74 | });
75 |
76 | test("should return the correct position on the first line of a document", () => {
77 | expect(
78 | findContainedSourceAndPosition(parts, {
79 | line: 4,
80 | character: 4,
81 | }),
82 | ).toEqual({ ...parts[0], position: { line: 0, character: 0 } });
83 | });
84 |
85 | test("should return the correct position on the last line of a document", () => {
86 | expect(
87 | findContainedSourceAndPosition(parts, {
88 | line: 10,
89 | character: 0,
90 | }),
91 | ).toEqual({ ...parts[0], position: { line: 6, character: 0 } });
92 | });
93 |
94 | test("should return null if the position is outside of the document", () => {
95 | expect(
96 | findContainedSourceAndPosition(parts, {
97 | line: 4,
98 | character: 3,
99 | }),
100 | ).toBeNull();
101 | expect(
102 | findContainedSourceAndPosition(parts, {
103 | line: 10,
104 | character: 1,
105 | }),
106 | ).toBeNull();
107 | });
108 |
109 | test("should return the correct position on a single line document", () => {
110 | expect(
111 | findContainedSourceAndPosition(parts, {
112 | line: 12,
113 | character: 46,
114 | }),
115 | ).toEqual({ ...parts[1], position: { line: 0, character: 21 } });
116 | });
117 | });
118 | describe("positionInContainingDocument", () => {
119 | const parts = handleFilePartUpdates(
120 | extractGraphQLSources(
121 | TextDocument.create("uri", "javascript", 1, testText),
122 | "gql",
123 | )!,
124 | [],
125 | );
126 |
127 | test("should return the correct position inside a document", () => {
128 | expect(
129 | positionInContainingDocument(parts[0].source, {
130 | line: 1,
131 | character: 3,
132 | }),
133 | ).toEqual({ line: 5, character: 3 });
134 | });
135 |
136 | test("should return the correct position on the first line of a document", () => {
137 | expect(
138 | positionInContainingDocument(parts[0].source, {
139 | line: 0,
140 | character: 0,
141 | }),
142 | ).toEqual({ line: 4, character: 4 });
143 | });
144 |
145 | test("should return the correct position on the last line of a document", () => {
146 | expect(
147 | positionInContainingDocument(parts[0].source, {
148 | line: 6,
149 | character: 0,
150 | }),
151 | ).toEqual({ line: 10, character: 0 });
152 | });
153 |
154 | test("should return the correct position on a single line document", () => {
155 | expect(
156 | positionInContainingDocument(parts[1].source, {
157 | line: 0,
158 | character: 21,
159 | }),
160 | ).toEqual({ line: 12, character: 46 });
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/language-server/utilities/__tests__/uri.ts:
--------------------------------------------------------------------------------
1 | import { normalizeURI } from "../uri";
2 |
3 | describe("Unix URIs", () => {
4 | // this is the format that `glob` returns on unix
5 | const uriToMatchForUnix = "/test/myFile.js";
6 |
7 | // single forward slash (unix)
8 | it("handles /Users/me URIs", () => {
9 | const uri = "/test/myFile.js";
10 | const parsed = normalizeURI(uri);
11 | expect(parsed).toEqual(uriToMatchForUnix);
12 | });
13 |
14 | // single escaped backslash
15 | // treat these as forward slashes?
16 | it("handles \\Users\\me URIs", () => {
17 | const uri = "\\test\\myFile.js";
18 | const parsed = normalizeURI(uri);
19 | expect(parsed).toEqual(uriToMatchForUnix);
20 | });
21 | });
22 |
23 | describe("Windows URIs", () => {
24 | // this is the format that `glob` returns for windows
25 | const uriToMatchForWindows = "c:/test/myFile.js";
26 |
27 | // this format is sent by the native extension notification system on windows
28 | it("handles file:///c%3A/ URIs", () => {
29 | const uri = "file:///c%3A/test/myFile.js";
30 | const parsed = normalizeURI(uri);
31 | expect(parsed).toEqual(uriToMatchForWindows);
32 | });
33 |
34 | // same as above without URI encoded :
35 | it("handles handles file:///c:/ URIs", () => {
36 | const uri = "file:///c:/test/myFile.js";
37 | const parsed = normalizeURI(uri);
38 | expect(parsed).toEqual(uriToMatchForWindows);
39 | });
40 |
41 | // result of glob.sync
42 | it("handles c:/ URIs", () => {
43 | const uri = "c:/test/myFile.js";
44 | const parsed = normalizeURI(uri);
45 | expect(parsed).toEqual(uriToMatchForWindows);
46 | });
47 |
48 | // from status bar notification
49 | // single (escaped) backslash
50 | it("handles c:\\ URIs", () => {
51 | const uri = "c:\\test\\myFile.js";
52 | const parsed = normalizeURI(uri);
53 | expect(parsed).toEqual(uriToMatchForWindows);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/language-server/utilities/debouncer.ts:
--------------------------------------------------------------------------------
1 | import debounce from "lodash.debounce";
2 |
3 | export function debounceHandler(
4 | handler: (...args: any[]) => any,
5 | leading: boolean = true,
6 | ) {
7 | return debounce(handler, 250, { leading });
8 | }
9 |
--------------------------------------------------------------------------------
/src/language-server/utilities/debug.ts:
--------------------------------------------------------------------------------
1 | import { LanguageServerNotifications as Notifications } from "../../messages";
2 | import { Connection, TraceValues } from "vscode-languageserver/node";
3 | import { format } from "util";
4 |
5 | /**
6 | * for errors (and other logs in debug mode) we want to print
7 | * a stack trace showing where they were thrown. This uses an
8 | * Error's stack trace, removes the three frames regarding
9 | * this file (since they're useless) and returns the rest of the trace.
10 | */
11 | const createAndTrimStackTrace = () => {
12 | let stack: string | undefined = new Error().stack;
13 | // remove the lines in the stack from _this_ function and the caller (in this file) and shorten the trace
14 | return stack && stack.split("\n").length > 2
15 | ? stack.split("\n").slice(3, 7).join("\n")
16 | : stack;
17 | };
18 |
19 | type Logger = (message?: any, minLevel?: TraceLevel) => void;
20 | export enum TraceLevel {
21 | "off" = 0,
22 | "messages" = 1,
23 | "verbose" = 2,
24 | }
25 |
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 | }
40 | private static connection?: Connection;
41 | private static infoLogger: Logger = (message) =>
42 | console.log("[INFO] " + message);
43 | private static warningLogger: Logger = (message) =>
44 | console.warn("[WARNING] " + message);
45 | private static errorLogger: Logger = (message) =>
46 | console.error("[ERROR] " + message);
47 |
48 | /**
49 | * Setting a connection overrides the default info/warning/error
50 | * loggers to pass a notification to the connection
51 | */
52 | public static SetConnection(conn: Connection) {
53 | Debug.connection = conn;
54 | Debug.infoLogger = (message) =>
55 | Debug.connection!.sendNotification(Notifications.ServerDebugMessage, {
56 | type: "info",
57 | message: message,
58 | });
59 | Debug.warningLogger = (message) =>
60 | Debug.connection!.sendNotification(Notifications.ServerDebugMessage, {
61 | type: "warning",
62 | message: message,
63 | });
64 | Debug.errorLogger = (message) =>
65 | Debug.connection!.sendNotification(Notifications.ServerDebugMessage, {
66 | type: "error",
67 | message: message,
68 | });
69 | }
70 |
71 | /**
72 | * Allow callers to set their own error logging utils.
73 | * These will default to console.log/warn/error
74 | */
75 | public static SetLoggers({
76 | info,
77 | warning,
78 | error,
79 | }: {
80 | info?: Logger;
81 | warning?: Logger;
82 | error?: Logger;
83 | }) {
84 | if (info) Debug.infoLogger = info;
85 | if (warning) Debug.warningLogger = warning;
86 | if (error) Debug.errorLogger = error;
87 | }
88 |
89 | public static info(message: string, ...param: any[]) {
90 | Debug.infoLogger(format(message, ...param));
91 | }
92 |
93 | public static error(message: string, ...param: any[]) {
94 | const stack = createAndTrimStackTrace();
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 | }
114 | }
115 |
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 | }
122 | }
123 |
124 | public static sendErrorTelemetry(message: string) {
125 | Debug.connection &&
126 | Debug.connection.sendNotification(Notifications.ServerDebugMessage, {
127 | type: "errorTelemetry",
128 | message: message,
129 | });
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/language-server/utilities/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./debouncer";
2 | export * from "./uri";
3 | export { Debug } from "./debug";
4 |
--------------------------------------------------------------------------------
/src/language-server/utilities/languageIdForExtension.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FileExtension,
3 | LanguageIdExtensionMap,
4 | supportedLanguageIds,
5 | } from "../../tools/utilities/languageInformation";
6 |
7 | let languageIdPerExtension: Record | undefined;
8 | let supportedExtensions: FileExtension[] | undefined;
9 |
10 | export function setLanguageIdExtensionMap(map: LanguageIdExtensionMap) {
11 | languageIdPerExtension = Object.fromEntries(
12 | Object.entries(map).flatMap(([languageId, extensions]) =>
13 | extensions.map((extension) => [extension, languageId]),
14 | ),
15 | );
16 | supportedExtensions = supportedLanguageIds.flatMap(
17 | (languageId) => map[languageId],
18 | );
19 | }
20 |
21 | /**
22 | * @throws if called before the language server has received options via `onInitialize`.
23 | */
24 | export function getLanguageIdForExtension(ext: FileExtension) {
25 | if (!languageIdPerExtension) {
26 | throw new Error("LanguageIdExtensionMap not set");
27 | }
28 | return languageIdPerExtension[ext];
29 | }
30 |
31 | /**
32 | * @throws if called before the language server has received options via `onInitialize`.
33 | */
34 | export function getSupportedExtensions() {
35 | if (!supportedExtensions) {
36 | throw new Error("LanguageIdExtensionMap not set");
37 | }
38 | return supportedExtensions;
39 | }
40 |
--------------------------------------------------------------------------------
/src/language-server/utilities/uri.ts:
--------------------------------------------------------------------------------
1 | import { URI } from "vscode-uri";
2 |
3 | export const withUnixSeparator = (uriString: string) =>
4 | uriString.split(/[\/\\]/).join("/");
5 |
6 | export const normalizeURI = (uriString: string) => {
7 | let parsed;
8 | if (uriString.indexOf("file:///") === 0) {
9 | parsed = URI.file(URI.parse(uriString).fsPath);
10 | } else if (uriString.match(/^[a-zA-Z]:[\/\\].*/)) {
11 | // uri with a drive prefix but not file:///
12 | parsed = URI.file(
13 | URI.parse("file:///" + withUnixSeparator(uriString)).fsPath,
14 | );
15 | } else {
16 | parsed = URI.parse(withUnixSeparator(uriString));
17 | }
18 | return withUnixSeparator(parsed.fsPath);
19 | };
20 |
--------------------------------------------------------------------------------
/src/languageServerClient.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ServerOptions,
3 | TransportKind,
4 | LanguageClientOptions,
5 | LanguageClient,
6 | RevealOutputChannelOn,
7 | } from "vscode-languageclient/node";
8 | import { workspace, OutputChannel } from "vscode";
9 | import { supportedLanguageIds } from "./tools/utilities/languageInformation";
10 | import type { InitializationOptions } from "./language-server/server";
11 | import { getLangugageInformation } from "./tools/utilities/getLanguageInformation";
12 |
13 | const { version, referenceID } = require("../package.json");
14 |
15 | const languageIdExtensionMap = getLangugageInformation();
16 | const supportedExtensions = supportedLanguageIds.flatMap(
17 | (id) => languageIdExtensionMap[id],
18 | );
19 |
20 | export function getLanguageServerClient(
21 | serverModule: string,
22 | outputChannel: OutputChannel,
23 | ) {
24 | const env = {
25 | APOLLO_CLIENT_NAME: "Apollo VS Code",
26 | APOLLO_CLIENT_VERSION: version,
27 | APOLLO_CLIENT_REFERENCE_ID: referenceID,
28 | NODE_TLS_REJECT_UNAUTHORIZED: 0,
29 | };
30 |
31 | const debugOptions = {
32 | execArgv: ["--nolazy", "--inspect=6009"],
33 | env,
34 | };
35 |
36 | const serverOptions: ServerOptions = {
37 | run: {
38 | module: serverModule,
39 | transport: TransportKind.ipc,
40 | options: {
41 | env,
42 | },
43 | },
44 | debug: {
45 | module: serverModule,
46 | transport: TransportKind.ipc,
47 | options: debugOptions,
48 | },
49 | };
50 |
51 | const clientOptions: LanguageClientOptions = {
52 | documentSelector: supportedLanguageIds,
53 | synchronize: {
54 | fileEvents: [
55 | workspace.createFileSystemWatcher("**/{.env?(.local)}"),
56 | workspace.createFileSystemWatcher("**/apollo.config.{json,yml,yaml}"),
57 | workspace.createFileSystemWatcher(
58 | "**/*{" + supportedExtensions.join(",") + "}",
59 | ),
60 | ],
61 | },
62 | outputChannel,
63 | revealOutputChannelOn: workspace
64 | .getConfiguration("apollographql")
65 | .get("debug.revealOutputOnLanguageServerError")
66 | ? RevealOutputChannelOn.Error
67 | : RevealOutputChannelOn.Never,
68 | initializationOptions: {
69 | languageIdExtensionMap,
70 | } satisfies InitializationOptions,
71 | };
72 |
73 | return new LanguageClient(
74 | "apollographql",
75 | "Apollo GraphQL",
76 | serverOptions,
77 | clientOptions,
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/messages.ts:
--------------------------------------------------------------------------------
1 | import { QuickPickItem } from "vscode";
2 | import { NotificationType, RequestType } from "vscode-jsonrpc";
3 | import { Range } from "vscode-languageclient/node";
4 |
5 | export interface TypeStats {
6 | service?: number;
7 | client?: number;
8 | total?: number;
9 | }
10 |
11 | export type ProjectStats =
12 | | {
13 | type: string;
14 | loaded: true;
15 | serviceId?: string;
16 | types?: TypeStats;
17 | tag?: string;
18 | lastFetch?: number;
19 | }
20 | | { loaded: false };
21 |
22 | export type EngineDecoration =
23 | | {
24 | type: "text";
25 | document: string;
26 | message: string;
27 | range: Range;
28 | }
29 | | {
30 | type: "runGlyph";
31 | document: string;
32 | range: Range;
33 | hoverMessage: string;
34 | };
35 |
36 | export const LanguageServerRequests = {
37 | FileStats: new RequestType<{ uri: string }, ProjectStats, unknown>(
38 | "apollographql/fileStats",
39 | ),
40 | };
41 |
42 | /**
43 | * Notifications sent to the language server
44 | */
45 | export const LanguageServerCommands = {
46 | GetStats: new NotificationType<{ uri: string }>("apollographql/getStats"),
47 | ReloadService: new NotificationType("apollographql/reloadService"),
48 | TagSelected: new NotificationType("apollographql/tagSelected"),
49 | };
50 |
51 | /**
52 | * Notifications sent from the language server
53 | */
54 | export const LanguageServerNotifications = {
55 | StatsLoaded: new NotificationType("apollographql/statsLoaded"),
56 | ConfigFilesFound: new NotificationType(
57 | "apollographql/configFilesFound",
58 | ),
59 | TagsLoaded: new NotificationType("apollographql/tagsLoaded"),
60 | LoadingComplete: new NotificationType(
61 | "apollographql/loadingComplete",
62 | ),
63 | Loading: new NotificationType<{
64 | message: string;
65 | token: number;
66 | }>("apollographql/loading"),
67 | EngineDecorations: new NotificationType<{
68 | decorations: EngineDecoration[];
69 | }>("apollographql/engineDecorations"),
70 | ServerDebugMessage: new NotificationType<{
71 | type: "info" | "warning" | "error" | "errorTelemetry";
72 | message: string;
73 | stack?: string;
74 | }>("serverDebugMessage"),
75 | };
76 |
--------------------------------------------------------------------------------
/src/statusBar.ts:
--------------------------------------------------------------------------------
1 | import { window, StatusBarAlignment } from "vscode";
2 |
3 | interface LoadingInput {
4 | hasActiveTextEditor: boolean;
5 | }
6 |
7 | interface StateChangeInput extends LoadingInput {
8 | text: string;
9 | tooltip?: string;
10 | }
11 |
12 | export default class ApolloStatusBar {
13 | public statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right);
14 |
15 | static loadingStateText = "Apollo $(rss)";
16 | static loadedStateText = "Apollo $(rocket)";
17 | static warningText = "Apollo $(alert)";
18 |
19 | constructor({ hasActiveTextEditor }: LoadingInput) {
20 | this.showLoadingState({ hasActiveTextEditor });
21 | this.statusBarItem.command = "apollographql/showStats";
22 | }
23 |
24 | protected changeState({
25 | hasActiveTextEditor,
26 | text,
27 | tooltip,
28 | }: StateChangeInput) {
29 | if (!hasActiveTextEditor) {
30 | this.statusBarItem.hide();
31 | return;
32 | }
33 |
34 | this.statusBarItem.text = text;
35 | this.statusBarItem.tooltip = tooltip;
36 | this.statusBarItem.show();
37 | }
38 |
39 | public showLoadingState({ hasActiveTextEditor }: LoadingInput) {
40 | this.changeState({
41 | hasActiveTextEditor,
42 | text: ApolloStatusBar.loadingStateText,
43 | });
44 | }
45 |
46 | public showLoadedState({ hasActiveTextEditor }: LoadingInput) {
47 | this.changeState({
48 | hasActiveTextEditor,
49 | text: ApolloStatusBar.loadedStateText,
50 | });
51 | }
52 |
53 | public showWarningState({
54 | hasActiveTextEditor,
55 | tooltip,
56 | }: LoadingInput & { tooltip: string }) {
57 | this.changeState({
58 | hasActiveTextEditor,
59 | text: ApolloStatusBar.warningText,
60 | tooltip,
61 | });
62 | }
63 |
64 | public dispose() {
65 | this.statusBarItem.dispose();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/tools/__tests__/snapshotSerializers/astSerializer.ts:
--------------------------------------------------------------------------------
1 | import { ASTNode, print } from "graphql";
2 | import { Plugin } from "pretty-format";
3 |
4 | const plugin: Plugin = {
5 | test(value) {
6 | return value && typeof value.kind === "string";
7 | },
8 |
9 | serialize(value: ASTNode, _config, indentation): string {
10 | return (
11 | indentation +
12 | print(value)
13 | .trim()
14 | .replace(/\n/g, "\n" + indentation)
15 | );
16 | },
17 | };
18 |
19 | export default plugin;
20 |
--------------------------------------------------------------------------------
/src/tools/__tests__/snapshotSerializers/graphQLTypeSerializer.ts:
--------------------------------------------------------------------------------
1 | import { isNamedType, GraphQLNamedType, printType } from "graphql";
2 | import { Plugin } from "pretty-format";
3 |
4 | const plugin: Plugin = {
5 | test(value) {
6 | return value && isNamedType(value);
7 | },
8 |
9 | serialize(value: GraphQLNamedType): string {
10 | return printType(value);
11 | },
12 | };
13 |
14 | export default plugin;
15 |
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
1 | import "../env";
2 |
3 | export * from "./utilities";
4 |
5 | export * from "./schema";
6 | export * from "./buildServiceDefinition";
7 |
--------------------------------------------------------------------------------
/src/tools/schema/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./resolverMap";
2 | export * from "./resolveObject";
3 |
--------------------------------------------------------------------------------
/src/tools/schema/resolveObject.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLResolveInfo, FieldNode } from "graphql";
2 |
3 | export type GraphQLObjectResolver = (
4 | source: TSource,
5 | fields: Record>,
6 | context: TContext,
7 | info: GraphQLResolveInfo,
8 | ) => any;
9 |
10 | declare module "graphql/type/definition" {
11 | interface GraphQLObjectType {
12 | resolveObject?: GraphQLObjectResolver;
13 | }
14 |
15 | interface GraphQLObjectTypeConfig {
16 | resolveObject?: GraphQLObjectResolver;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/tools/schema/resolverMap.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLFieldResolver } from "graphql";
2 |
3 | export interface GraphQLResolverMap {
4 | [typeName: string]: {
5 | [fieldName: string]:
6 | | GraphQLFieldResolver
7 | | {
8 | requires?: string;
9 | resolve: GraphQLFieldResolver;
10 | subscribe?: undefined;
11 | }
12 | | {
13 | requires?: string;
14 | resolve?: undefined;
15 | subscribe: GraphQLFieldResolver;
16 | }
17 | | {
18 | requires?: string;
19 | resolve: GraphQLFieldResolver;
20 | subscribe: GraphQLFieldResolver;
21 | };
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/tools/utilities/getLanguageInformation.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import {
3 | LanguageIdExtensionMap,
4 | minimumKnownExtensions,
5 | } from "./languageInformation";
6 |
7 | /**
8 | * @returns An object with language identifiers as keys and file extensions as values.
9 | * see https://github.com/microsoft/vscode/issues/109919
10 | */
11 | export function getLangugageInformation(): LanguageIdExtensionMap {
12 | const allKnownExtensions = vscode.extensions.all
13 | .map(
14 | (i) =>
15 | i.packageJSON?.contributes?.languages as (
16 | | undefined
17 | | {
18 | id?: string;
19 | extensions?: string[];
20 | }
21 | )[],
22 | )
23 | .flat()
24 | .filter(
25 | (i): i is { id: string; extensions: `.${string}`[] } =>
26 | !!(i && i.id && i.extensions?.length),
27 | )
28 | .reduce>>(
29 | (acc, i) => {
30 | if (!acc[i.id]) acc[i.id] = new Set();
31 | for (const ext of i.extensions) acc[i.id].add(ext);
32 | return acc;
33 | },
34 | Object.fromEntries(
35 | Object.entries(minimumKnownExtensions).map(([k, v]) => [k, new Set(v)]),
36 | ),
37 | );
38 | return Object.fromEntries(
39 | Object.entries(allKnownExtensions).map(([k, v]) => [k, [...v]] as const),
40 | ) as LanguageIdExtensionMap;
41 | }
42 |
--------------------------------------------------------------------------------
/src/tools/utilities/graphql.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ASTNode,
3 | TypeDefinitionNode,
4 | TypeExtensionNode,
5 | DocumentNode,
6 | Kind,
7 | } from "graphql";
8 |
9 | // FIXME: We should add proper type guards for these predicate functions
10 | // to `@types/graphql`.
11 | declare module "graphql/language/predicates" {
12 | function isTypeDefinitionNode(node: ASTNode): node is TypeDefinitionNode;
13 | function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode;
14 | }
15 |
16 | export function isNode(maybeNode: any): maybeNode is ASTNode {
17 | return maybeNode && typeof maybeNode.kind === "string";
18 | }
19 |
20 | export function isDocumentNode(node: ASTNode): node is DocumentNode {
21 | return isNode(node) && node.kind === Kind.DOCUMENT;
22 | }
23 |
--------------------------------------------------------------------------------
/src/tools/utilities/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./invariant";
2 | export * from "./predicates";
3 | export * from "./graphql";
4 |
--------------------------------------------------------------------------------
/src/tools/utilities/invariant.ts:
--------------------------------------------------------------------------------
1 | export function invariant(condition: any, message: string) {
2 | if (!condition) {
3 | throw new Error(message);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/tools/utilities/languageInformation.ts:
--------------------------------------------------------------------------------
1 | const _supportedDocumentTypes = [
2 | "graphql",
3 | "javascript",
4 | "typescript",
5 | "javascriptreact",
6 | "typescriptreact",
7 | "vue",
8 | "svelte",
9 | "python",
10 | "ruby",
11 | "dart",
12 | "reason",
13 | "elixir",
14 | ] as const;
15 | export type SupportedLanguageIds = (typeof _supportedDocumentTypes)[number];
16 | export const supportedLanguageIds =
17 | // remove the `readonly` we get from using `as const`
18 | _supportedDocumentTypes as any as SupportedLanguageIds[];
19 |
20 | export type FileExtension = `.${string}`;
21 |
22 | export const minimumKnownExtensions: Record<
23 | SupportedLanguageIds,
24 | FileExtension[]
25 | > = {
26 | graphql: [".gql", ".graphql", ".graphqls"],
27 | javascript: [".js", ".mjs", ".cjs"],
28 | typescript: [".ts", ".mts", ".cts"],
29 | javascriptreact: [".jsx"],
30 | typescriptreact: [".tsx"],
31 | vue: [".vue"],
32 | svelte: [".svelte"],
33 | python: [".py", ".ipynb"],
34 | ruby: [".rb"],
35 | dart: [".dart"],
36 | reason: [".re"],
37 | elixir: [".ex", ".exs"],
38 | };
39 |
40 | export type LanguageIdExtensionMap = Record &
41 | typeof minimumKnownExtensions;
42 |
--------------------------------------------------------------------------------
/src/tools/utilities/predicates.ts:
--------------------------------------------------------------------------------
1 | export function isNotNullOrUndefined(
2 | value: T | null | undefined,
3 | ): value is T {
4 | return value !== null && typeof value !== "undefined";
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { LanguageClient } from "vscode-languageclient/node";
2 | import type { ProjectStats } from "./messages";
3 |
4 | export const timeSince = (date: number) => {
5 | const seconds = Math.floor((+new Date() - date) / 1000);
6 | if (!seconds) return;
7 | let interval = Math.floor(seconds / 86400);
8 | if (interval >= 1) return `${interval}d`;
9 |
10 | interval = Math.floor(seconds / 3600);
11 | if (interval >= 1) return `${interval}h`;
12 |
13 | interval = Math.floor(seconds / 60);
14 | if (interval >= 1) return `${interval}m`;
15 |
16 | return `${Math.floor(seconds)}s`;
17 | };
18 |
19 | export const printNoFileOpenMessage = (
20 | client: LanguageClient,
21 | extVersion: string,
22 | ) => {
23 | client.outputChannel.appendLine("------------------------------");
24 | client.outputChannel.appendLine(`🚀 Apollo GraphQL v${extVersion}`);
25 | client.outputChannel.appendLine("------------------------------");
26 | };
27 |
28 | export const printStatsToClientOutputChannel = (
29 | client: LanguageClient,
30 | stats: ProjectStats,
31 | extVersion: string,
32 | ) => {
33 | client.outputChannel.appendLine("------------------------------");
34 | client.outputChannel.appendLine(`🚀 Apollo GraphQL v${extVersion}`);
35 | client.outputChannel.appendLine("------------------------------");
36 |
37 | if (!stats || !stats.loaded) {
38 | client.outputChannel.appendLine(
39 | "❌ Service stats could not be loaded. This may be because you're missing an apollo.config.js file " +
40 | "or it is misconfigured. For more information about configuring Apollo projects, " +
41 | "see the guide here (https://go.apollo.dev/t/config).",
42 | );
43 | return;
44 | }
45 |
46 | // we don't support logging of stats for service projects currently
47 | if (stats.type === "service") {
48 | return;
49 | } else if (stats.type === "client") {
50 | client.outputChannel.appendLine("✅ Service Loaded!");
51 | client.outputChannel.appendLine(`🆔 Service ID: ${stats.serviceId}`);
52 | client.outputChannel.appendLine(`🏷 Schema Tag: ${stats.tag}`);
53 |
54 | if (stats.types)
55 | client.outputChannel.appendLine(
56 | `📈 Number of Types: ${stats.types.total} (${
57 | stats.types.client
58 | } client ${stats.types.client === 1 ? "type" : "types"})`,
59 | );
60 |
61 | if (stats.lastFetch && timeSince(stats.lastFetch)) {
62 | client.outputChannel.appendLine(
63 | `🗓 Last Fetched ${timeSince(stats.lastFetch)} Ago`,
64 | );
65 | }
66 | client.outputChannel.appendLine("------------------------------");
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/start-ac.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // for testing, start a few of these, e.g. with
4 | // while true; do echo "foo\nbar\nbaz" | parallel ./start-ac.mjs; sleep 1; done
5 |
6 | import { connectApolloClientToVSCodeDevTools } from "@apollo/client-devtools-vscode";
7 | import WebSocket from "ws";
8 | import { ApolloClient, InMemoryCache } from "@apollo/client/core/index.js";
9 | import { MockLink } from "@apollo/client/testing/core/index.js";
10 | import gql from "graphql-tag";
11 |
12 | globalThis.WebSocket ||= WebSocket;
13 |
14 | const helloWorld = gql`
15 | query {
16 | hello
17 | }
18 | `;
19 |
20 | const link = new MockLink([
21 | {
22 | request: { query: helloWorld },
23 | result: { data: { hello: "world" } },
24 | maxUsageCount: 1000,
25 | },
26 | {
27 | request: {
28 | query: gql`
29 | query {
30 | hi
31 | }
32 | `,
33 | },
34 | result: { data: { hi: "universe" } },
35 | maxUsageCount: 1000,
36 | },
37 | ]);
38 | const client = new ApolloClient({
39 | link,
40 | cache: new InMemoryCache(),
41 | devtools: { name: process.argv[2] },
42 | });
43 | client.watchQuery({ query: helloWorld }).subscribe({ next() {} });
44 | const { connectedPromise, disconnect, onCleanup } =
45 | connectApolloClientToVSCodeDevTools(
46 | client,
47 | "ws://localhost:7095", // nosemgrep
48 | );
49 | console.log("connecting...");
50 | onCleanup((reason) =>
51 | console.log(
52 | "disconnected",
53 | reason,
54 | /* referencing client here to prevent it from getting garbage connected */ client.version,
55 | ),
56 | );
57 | connectedPromise.then(() => {
58 | console.log("connected");
59 | // setTimeout(unregister, 5000, "USERLAND_TIMEOUT");
60 | });
61 |
--------------------------------------------------------------------------------
/syntaxes/graphql.dart.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["dart"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "name": "meta.function-call.dart",
7 | "begin": "\\b(gql)(\\()",
8 | "beginCaptures": {
9 | "1": {
10 | "name": "entity.name.function.dart"
11 | },
12 | "2": {
13 | "name": "punctuation.definition.arguments.begin.dart"
14 | }
15 | },
16 | "end": "(\\))",
17 | "endCaptures": {
18 | "1": {
19 | "name": "punctuation.definition.arguments.end.dart"
20 | }
21 | },
22 | "patterns": [
23 | {
24 | "name": "taggedTemplates",
25 | "contentName": "meta.embedded.block.graphql",
26 | "begin": "r?(\"\"\"|''')",
27 | "end": "((\\1))",
28 | "patterns": [
29 | {
30 | "include": "source.graphql"
31 | }
32 | ]
33 | }
34 | ]
35 | }
36 | ],
37 | "scopeName": "inline.graphql.dart"
38 | }
39 |
--------------------------------------------------------------------------------
/syntaxes/graphql.ex.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["ex", "exs"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "name": "meta.function-call.elixir",
7 | "begin": "\\b(gql)(\\()",
8 | "beginCaptures": {
9 | "1": {
10 | "name": "entity.name.function.elixir"
11 | },
12 | "2": {
13 | "name": "punctuation.definition.arguments.begin.elixir"
14 | }
15 | },
16 | "end": "(\\))",
17 | "endCaptures": {
18 | "1": {
19 | "name": "punctuation.definition.arguments.end.elixir"
20 | }
21 | },
22 | "patterns": [
23 | {
24 | "name": "taggedTemplates",
25 | "contentName": "meta.embedded.block.graphql",
26 | "begin": "r?(\"\"\")",
27 | "end": "((\\1))",
28 | "patterns": [
29 | {
30 | "include": "source.graphql"
31 | }
32 | ]
33 | }
34 | ]
35 | }
36 | ],
37 | "scopeName": "inline.graphql.elixir"
38 | }
39 |
--------------------------------------------------------------------------------
/syntaxes/graphql.js.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["js", "jsx", "ts", "tsx", "vue", "svelte"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "contentName": "meta.embedded.block.graphql",
7 | "begin": "(?:Relay\\.QL|gql|graphql(\\.experimental)?)\\s*(?:<.*?>\\s*)?`",
8 | "end": "`",
9 | "patterns": [{ "include": "source.graphql" }]
10 | },
11 | {
12 | "contentName": "meta.embedded.block.graphql",
13 | "begin": "`(\\s*#[ ]*(gql|graphql|GraphQL))",
14 | "beginCaptures": {
15 | "1": {
16 | "name": "meta.embedded.block.graphql comment.line.graphql.js"
17 | },
18 | "2": {
19 | "name": "markup.italic"
20 | }
21 | },
22 | "end": "`",
23 | "patterns": [{ "include": "source.graphql" }]
24 | },
25 | {
26 | "contentName": "meta.embedded.block.graphql",
27 | "begin": "(?:gql|graphql)\\s*(?:<.*?>\\s*)?\\(\\s*`",
28 | "end": "`\\s*\\)",
29 | "patterns": [{ "include": "source.graphql" }]
30 | },
31 | {
32 | "contentName": "meta.embedded.block.graphql",
33 | "begin": "(?:\\/\\*[\\s\\*]*(gql|graphql|GraphQL)\\s*\\*\\/)\\s*(`)",
34 | "beginCaptures": {
35 | "1": {
36 | "name": "markup.italic"
37 | }
38 | },
39 | "end": "`",
40 | "patterns": [{ "include": "source.graphql" }]
41 | }
42 | ],
43 | "scopeName": "inline.graphql"
44 | }
45 |
--------------------------------------------------------------------------------
/syntaxes/graphql.lua.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["lua", "luau"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "contentName": "meta.embedded.block.graphql",
7 | "begin": "(--\\[\\[)\\s*(gql)\\s*(\\]\\])\\s*(\\[\\[)",
8 | "beginCaptures": {
9 | "1": {
10 | "name": "comment.block.lua"
11 | },
12 | "2": {
13 | "name": "entity.name.function.lua"
14 | },
15 | "3": {
16 | "name": "comment.block.lua"
17 | },
18 | "4": {
19 | "name": "string.quoted.double.lua"
20 | }
21 | },
22 | "end": "(\\]\\])",
23 | "endCaptures": {
24 | "1": {
25 | "name": "string.quoted.double.lua"
26 | }
27 | },
28 | "patterns": [{ "include": "source.graphql" }]
29 | },
30 | {
31 | "contentName": "meta.embedded.block.graphql",
32 | "begin": "(gql)\\(?\\s*(\\[\\[)",
33 | "beginCaptures": {
34 | "1": {
35 | "name": "entity.name.function.lua"
36 | },
37 | "2": {
38 | "name": "string.quoted.double.lua"
39 | }
40 | },
41 | "end": "(\\]\\])",
42 | "endCaptures": {
43 | "1": {
44 | "name": "string.quoted.double.lua"
45 | }
46 | },
47 | "patterns": [{ "include": "source.graphql" }]
48 | }
49 | ],
50 | "scopeName": "inline.graphql.lua"
51 | }
52 |
--------------------------------------------------------------------------------
/syntaxes/graphql.py.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["python"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "name": "meta.function-call.python",
7 | "begin": "\\b(gql)\\s*(\\()",
8 | "beginCaptures": {
9 | "1": {
10 | "name": "meta.function-call.generic.python"
11 | },
12 | "2": {
13 | "name": "punctuation.definition.arguments.begin.python"
14 | }
15 | },
16 | "end": "(\\))",
17 | "endCaptures": {
18 | "1": {
19 | "name": "punctuation.definition.arguments.end.python"
20 | }
21 | },
22 | "patterns": [
23 | {
24 | "name": "taggedTemplates",
25 | "contentName": "meta.embedded.block.graphql",
26 | "begin": "([bfru]*)((\"(?:\"\")?|'(?:'')?))",
27 | "beginCaptures": {
28 | "1": {
29 | "name": "storage.type.string.python"
30 | },
31 | "2": {
32 | "name": "string.quoted.multi.python"
33 | },
34 | "3": {
35 | "name": "punctuation.definition.string.begin.python"
36 | }
37 | },
38 | "end": "((\\3))",
39 | "endCaptures": {
40 | "1": {
41 | "name": "string.quoted.multi.python"
42 | },
43 | "2": {
44 | "name": "punctuation.definition.string.end.python"
45 | }
46 | },
47 | "patterns": [
48 | {
49 | "include": "source.graphql"
50 | }
51 | ]
52 | }
53 | ]
54 | }
55 | ],
56 | "scopeName": "inline.graphql.python"
57 | }
58 |
--------------------------------------------------------------------------------
/syntaxes/graphql.rb.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["rb"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "contentName": "meta.embedded.block.graphql",
7 | "begin": "(?><<[-~](['\"`]?)((?:[_\\w]+_|)GRAPHQL)\\b\\1)",
8 | "beginCaptures": {
9 | "0": {
10 | "name": "punctuation.definition.string.begin.ruby"
11 | }
12 | },
13 | "end": "\\s*\\2$\\n?",
14 | "endCaptures": {
15 | "0": {
16 | "name": "punctuation.definition.string.end.ruby"
17 | }
18 | },
19 | "patterns": [{ "include": "source.graphql" }]
20 | }
21 | ],
22 | "scopeName": "inline.graphql.ruby"
23 | }
24 |
--------------------------------------------------------------------------------
/syntaxes/graphql.re.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileTypes": ["re"],
3 | "injectionSelector": "L:source -string -comment",
4 | "patterns": [
5 | {
6 | "contentName": "meta.embedded.block.graphql",
7 | "begin": "(\\[%(graphql|relay\\.([a-zA-Z]*)))s*$",
8 | "end": "(?<=])",
9 | "patterns": [
10 | {
11 | "begin": "^\\s*({\\|)$",
12 | "end": "^\\s*(\\|})",
13 | "patterns": [{ "include": "source.graphql" }]
14 | }
15 | ]
16 | }
17 | ],
18 | "scopeName": "inline.graphql.reason"
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "./lib",
5 | "**/__tests__/*",
6 | "**/__e2e__/*",
7 | "**/__mocks__/*",
8 | "./sampleWorkspace",
9 | "jest.config.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "./src",
4 | "outDir": "./lib",
5 | "target": "es2020",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "sourceMap": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "removeComments": true,
13 | "allowJs": true,
14 | "strict": true,
15 | "noImplicitAny": true,
16 | "noImplicitReturns": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noUnusedParameters": false,
19 | "noUnusedLocals": false,
20 | "forceConsistentCasingInFileNames": true,
21 | "lib": ["es2020", "esnext.asynciterable"],
22 | "types": ["node", "jest"],
23 | "baseUrl": "."
24 | },
25 | "include": ["./src"],
26 | "exclude": ["./lib", "jest.config.ts", "./sampleWorkspace"]
27 | }
28 |
--------------------------------------------------------------------------------