diff --git a/.changeset/config.json b/.changeset/config.json index 4a40bb63b8e..36fce60cf83 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,13 +1,18 @@ { - "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", + "$schema": "https://unpkg.com/@changesets/config@2.0.1/schema.json", "changelog": ["@changesets/changelog-github", { "repo": "graphql/graphiql" }], "commit": false, "linked": [], "access": "public", "baseBranch": "main", - "ignore": ["example-*"], + "ignore": [ + "example-graphiql-webpack", + "example-monaco-graphql-nextjs", + "example-monaco-graphql-react-vite", + "example-monaco-graphql-webpack" + ], "updateInternalDependencies": "patch", "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { - "updateInternalDependents": "always" + "onlyUpdatePeerDependentsWhenOutOfRange": true } } diff --git a/.eslintignore b/.eslintignore index ac46f475f89..d9f524de260 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ -.changeset/ +.changeset/*.md # ignore working-group dir markdown so it's easier for people to edit from the UI working-group/ @@ -6,3 +6,4 @@ packages/codemirror-graphql/src/__tests__/schema-kitchen-sink.graphql CHANGELOG.md **/CHANGELOG.md packages/vscode-graphql-syntax/tests/__fixtures__/ +packages/graphql-language-service-server/src/__tests__/parseDocument.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index f9f7e78023c..0a9cdf1f0ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,7 +35,7 @@ module.exports = { overrides: [ { // Rules for all code files - files: ['**/*.{js,jsx,ts,tsx}'], + files: ['**/*.{js,jsx,ts,tsx,mts,cts}'], parserOptions: { ecmaVersion: 6, }, @@ -54,21 +54,14 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', + 'plugin:import-x/recommended', + 'plugin:import-x/typescript', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:react/jsx-runtime', 'prettier', ], - plugins: [ - 'promise', - 'sonarjs', - 'unicorn', - '@arthurgeron/react-usememo', - 'sonar', - '@shopify', - ], + plugins: ['promise', 'sonarjs', 'unicorn', 'sonar', '@shopify'], globals: { atom: false, document: false, @@ -81,10 +74,6 @@ module.exports = { '@shopify/prefer-early-return': ['error', { maximumStatements: 2 }], '@shopify/prefer-class-properties': 'off', // enable after https://github.com/Shopify/web-configs/issues/387 will be fixed 'sonarjs/no-inverted-boolean-check': 'error', - '@arthurgeron/react-usememo/require-usememo': [ - 'error', - { checkHookCalls: false }, - ], // Possible Errors (http://eslint.org/docs/rules/#possible-errors) 'no-console': 'error', 'no-constant-binary-expression': 'error', @@ -133,7 +122,14 @@ module.exports = { 'no-octal-escape': 'error', 'no-param-reassign': 'error', 'no-proto': 'error', - 'no-restricted-properties': 'off', + 'no-restricted-properties': [ + 'error', + { + object: 'window', + property: 'localStorage', + message: 'Use `localStorage` instead', + }, + ], 'no-return-assign': 'error', 'no-return-await': 'error', 'no-script-url': 'error', @@ -144,7 +140,6 @@ module.exports = { 'no-useless-call': 'error', 'no-useless-concat': 'error', 'no-useless-return': 'off', - '@typescript-eslint/prefer-optional-chain': 'error', 'no-warning-comments': 'off', radix: 'error', 'require-await': 'off', @@ -281,9 +276,9 @@ module.exports = { 'sonarjs/no-ignored-return': 'error', 'unicorn/no-array-push-push': 'error', - 'import/no-extraneous-dependencies': 'error', - 'import/no-duplicates': 'error', - 'import/no-named-as-default': 'error', + 'import-x/no-extraneous-dependencies': 'error', + 'import-x/no-duplicates': 'error', + 'import-x/no-named-as-default': 'error', 'prefer-object-spread': 'error', // React rules 'react/no-unused-state': 'error', @@ -336,7 +331,15 @@ module.exports = { 'sonar/prefer-promise-shorthand': 'error', 'sonar/no-dead-store': 'error', 'unicorn/prefer-node-protocol': 'error', - 'import/no-unresolved': ['error', { ignore: ['^node:'] }], + 'import-x/no-unresolved': [ + 'error', + { ignore: ['^node:', '\\.svg\\?react$'] }, + ], + 'no-extra-boolean-cast': [ + 'error', + { enforceForInnerExpressions: true }, + ], + 'unicorn/no-length-as-slice-end': 'error', 'unicorn/prefer-string-replace-all': 'error', 'unicorn/no-hex-escape': 'off', // TODO: enable // doesn't catch a lot of cases; we use ESLint builtin `no-restricted-syntax` to forbid `.keyCode` @@ -351,14 +354,16 @@ module.exports = { }, { // Rules that requires type information - files: ['**/*.{ts,tsx}'], + files: ['**/*.{ts,tsx,mts,cts}'], excludedFiles: ['**/*.{md,mdx}/*.{ts,tsx}'], // extends: ['plugin:@typescript-eslint/recommended-type-checked'], rules: { + '@typescript-eslint/prefer-optional-chain': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/no-duplicate-type-constituents': 'error', // TODO: Fix all errors for the following rules included in recommended config '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-non-null-assertion': 'off', @@ -398,7 +403,6 @@ module.exports = { rules: { 'jest/no-conditional-expect': 'off', 'jest/expect-expect': ['error', { assertFunctionNames: ['expect*'] }], - '@arthurgeron/react-usememo/require-usememo': 'off', }, }, { @@ -415,14 +419,14 @@ module.exports = { 'no-console': 'off', 'no-new': 'off', 'no-alert': 'off', - 'import/no-unresolved': 'off', + 'import-x/no-unresolved': 'off', }, }, { // Rule for ignoring imported dependencies from tests files files: ['**/__tests__/**', 'webpack.config.js', '**/tests/**'], rules: { - 'import/no-extraneous-dependencies': 'off', + 'import-x/no-extraneous-dependencies': 'off', }, }, { @@ -432,7 +436,7 @@ module.exports = { 'packages/vscode-graphql-execution/**', ], rules: { - 'import/no-unresolved': ['error', { ignore: ['^node:', 'vscode'] }], + 'import-x/no-unresolved': ['error', { ignore: ['^node:', 'vscode'] }], }, }, { @@ -473,14 +477,13 @@ module.exports = { // Rules for codeblocks inside Markdown/MDX files: ['**/*.{md,mdx}/*.{js,jsx,ts,tsx}'], rules: { - 'import/no-extraneous-dependencies': 'off', + 'import-x/no-extraneous-dependencies': 'off', '@typescript-eslint/no-unused-vars': 'off', - 'import/no-unresolved': 'off', + 'import-x/no-unresolved': 'off', 'no-console': 'off', 'no-undef': 'off', 'react/jsx-no-undef': 'off', 'react-hooks/rules-of-hooks': 'off', - '@arthurgeron/react-usememo/require-usememo': 'off', 'sonar/no-dead-store': 'off', '@typescript-eslint/no-restricted-imports': 'off', }, diff --git a/.github/workflows/main-test.yml b/.github/workflows/main-test.yml index 7a93be632ca..53e4db10da6 100644 --- a/.github/workflows/main-test.yml +++ b/.github/workflows/main-test.yml @@ -10,23 +10,22 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 16 cache: yarn - name: Cache node modules id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cache/Cypress - key: cypress-${{ runner.os }} + key: cypress-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile --immutable jest: @@ -34,18 +33,17 @@ jobs: runs-on: ubuntu-latest needs: [install] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - run: yarn test --coverage - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage/lcov.info diff --git a/.github/workflows/pr-graphql-compat-check.yml b/.github/workflows/pr-graphql-compat-check.yml index 7c2d7ead6f0..7643e219f43 100644 --- a/.github/workflows/pr-graphql-compat-check.yml +++ b/.github/workflows/pr-graphql-compat-check.yml @@ -23,17 +23,17 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - release: ['15.5.3', '^15.8.0', '16.1.0', '16.2.0', '16.3.0'] + release: + ['15.5.3', '^15.8.0', '16.1.0', '16.2.0', '16.3.0', '17.0.0-alpha.5'] steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: cache: yarn - node-version: 16 - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: node_modules key: node_modules-${{hashFiles('yarn.lock')}} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b6c35bd1a43..5c7690b69dc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,23 +10,22 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 16 cache: yarn - name: Cache node modules id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cache/Cypress - key: cypress-${{ runner.os }} + key: cypress-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile --immutable build: @@ -35,19 +34,18 @@ jobs: needs: [install] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - run: yarn build - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: build-${{ github.sha }} path: ${{ env.BUILD-CACHE-LIST }} @@ -57,12 +55,10 @@ jobs: runs-on: ubuntu-latest needs: [install] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules @@ -74,12 +70,10 @@ jobs: runs-on: ubuntu-latest needs: [install] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules @@ -87,22 +81,20 @@ jobs: - run: yarn pretty-check jest: - name: Jest Unit Tests + name: Jest Unit & Integration Tests runs-on: ubuntu-latest needs: [install] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - run: yarn test --coverage - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage/lcov.info @@ -115,18 +107,17 @@ jobs: needs: [build] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: build-${{ github.sha }} path: ${{ env.BUILD-CACHE-LIST }} @@ -138,17 +129,16 @@ jobs: needs: [build] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: build-${{ github.sha }} path: ${{ env.BUILD-CACHE-LIST }} @@ -160,34 +150,34 @@ jobs: needs: [build] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: build-${{ github.sha }} path: ${{ env.BUILD-CACHE-LIST }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: | ~/.cache/Cypress - key: cypress-${{ runner.os }} + key: cypress-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Cypress run - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 with: install: false command: yarn e2e - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-screenshots path: packages/graphiql/cypress/screenshots if-no-files-found: ignore - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-videos @@ -201,19 +191,17 @@ jobs: needs: [build, jest, eslint, vitest, e2e] if: github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/setup-node@v4 - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules key: modules-${{ github.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: build-${{ github.sha }} path: ${{ env.BUILD-CACHE-LIST }} @@ -262,12 +250,10 @@ jobs: needs: [install] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - id: cache-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a84a1e070be..aa44f00b12a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,7 @@ name: Release on: push: - branches: - - main + branches: [main, graphiql-v4] permissions: {} jobs: release: @@ -14,15 +13,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: 16 cache: yarn - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-modules with: path: node_modules diff --git a/.nvmrc b/.nvmrc index 19c7bdba7b1..209e3ef4b62 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 \ No newline at end of file +20 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..5ded8c95b13 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# removing this will break tests b/c of whitespace changes + format on save/commit, etc +packages/graphql-language-service-server/src/__tests__/parseDocument.test.ts diff --git a/.prettierrc b/.prettierrc index a219e18edf6..d0ab7b6eb11 100644 --- a/.prettierrc +++ b/.prettierrc @@ -10,6 +10,12 @@ "printWidth": 80, "proseWrap": "preserve" } + }, + { + "files": "*.svg", + "options": { + "parser": "html" + } } ] } diff --git a/README.md b/README.md index 237dfa2f167..523ed4efde8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![GraphQLConf 2024 Banner: September 10-12, San Francisco. Hosted by the GraphQL Foundation](https://github.com/user-attachments/assets/bdb8cd5d-5186-4ece-b06b-b00a499b7868)](https://graphql.org/conf/2024/?utm_source=github&utm_medium=graphiql&utm_campaign=readme) + # GraphQL IDE Monorepo diff --git a/babel.config.js b/babel.config.js index 56fedcc08d2..50347139e3d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -6,7 +6,7 @@ const envConfig = { if (process.env.ESM) { envConfig.modules = false; - envConfig.targets = { node: true }; + envConfig.targets = { node: 'current' }; envConfig.bugfixes = true; } @@ -36,7 +36,10 @@ module.exports = { }, plugins: [ require.resolve('@babel/plugin-proposal-class-properties'), + require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), require.resolve('@babel/plugin-proposal-optional-chaining'), + require.resolve('@babel/plugin-transform-private-methods'), + ['babel-plugin-transform-import-meta', { module: 'ES6' }], ], }; diff --git a/cspell.json b/cspell.json index 24ff11ab942..9b714e08196 100644 --- a/cspell.json +++ b/cspell.json @@ -9,7 +9,7 @@ "dictionaryDefinitions": [ { "name": "custom-words", - "path": "./custom-words.txt", + "path": "./resources/custom-words.txt", "addWords": true } ], diff --git a/examples/cm6-graphql-legacy-parcel/package.json b/examples/cm6-graphql-legacy-parcel/package.json index 320173bbe95..7397dfc7d0f 100644 --- a/examples/cm6-graphql-legacy-parcel/package.json +++ b/examples/cm6-graphql-legacy-parcel/package.json @@ -1,6 +1,6 @@ { "name": "example-cm6-graphql-legacy-parcel", - "version": "1.1.10-alpha.8", + "version": "0.0.0", "license": "MIT", "description": "GraphiQL Parcel Example", "main": "index.js", diff --git a/examples/cm6-graphql-parcel/package.json b/examples/cm6-graphql-parcel/package.json index 890d04885d1..416f31729ec 100644 --- a/examples/cm6-graphql-parcel/package.json +++ b/examples/cm6-graphql-parcel/package.json @@ -1,6 +1,6 @@ { "name": "example-cm6-graphql-parcel", - "version": "1.1.10-alpha.8", + "version": "0.0.0", "license": "MIT", "description": "GraphiQL Parcel Example", "main": "index.js", diff --git a/examples/graphiql-cdn/package.json b/examples/graphiql-cdn/package.json index fb2b68a60d1..1ad81c99f97 100644 --- a/examples/graphiql-cdn/package.json +++ b/examples/graphiql-cdn/package.json @@ -1,6 +1,6 @@ { "name": "example-graphiql-cdn", - "version": "0.0.8", + "version": "0.0.0", "private": true, "license": "MIT", "description": "An example using GraphiQL", diff --git a/examples/graphiql-create-react-app/CHANGELOG.md b/examples/graphiql-create-react-app/CHANGELOG.md deleted file mode 100644 index 2f39755401f..00000000000 --- a/examples/graphiql-create-react-app/CHANGELOG.md +++ /dev/null @@ -1,68 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.1.11-alpha.8](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.7...example-graphiql-create-react-app@0.1.11-alpha.8) (2021-01-07) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.7](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.6...example-graphiql-create-react-app@0.1.11-alpha.7) (2021-01-07) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.6](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.5...example-graphiql-create-react-app@0.1.11-alpha.6) (2021-01-07) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.5](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.4...example-graphiql-create-react-app@0.1.11-alpha.5) (2021-01-03) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.4](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.3...example-graphiql-create-react-app@0.1.11-alpha.4) (2020-12-28) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.3](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.2...example-graphiql-create-react-app@0.1.11-alpha.3) (2020-08-26) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.2](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.1...example-graphiql-create-react-app@0.1.11-alpha.2) (2020-08-22) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.1](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.0...example-graphiql-create-react-app@0.1.11-alpha.1) (2020-08-12) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.11-alpha.0](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.10...example-graphiql-create-react-app@0.1.11-alpha.0) (2020-08-10) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.10](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.6...example-graphiql-create-react-app@0.1.10) (2020-08-06) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.6](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.5...example-graphiql-create-react-app@0.1.6) (2020-06-11) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.5](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.4...example-graphiql-create-react-app@0.1.5) (2020-06-04) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.4](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.3...example-graphiql-create-react-app@0.1.4) (2020-06-04) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.3](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.2...example-graphiql-create-react-app@0.1.3) (2020-05-28) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## [0.1.2](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.1...example-graphiql-create-react-app@0.1.2) (2020-05-19) - -**Note:** Version bump only for package example-graphiql-create-react-app - -## 0.1.1 (2020-05-17) - -**Note:** Version bump only for package example-graphiql-create-react-app diff --git a/examples/graphiql-create-react-app/package.json b/examples/graphiql-create-react-app/package.json index beac9385e65..e46b92ee19e 100644 --- a/examples/graphiql-create-react-app/package.json +++ b/examples/graphiql-create-react-app/package.json @@ -1,12 +1,12 @@ { "name": "example-graphiql-create-react-app", - "version": "0.1.11-alpha.8", + "version": "0.0.0", "private": true, "dependencies": { - "graphiql": "^2.2.0", - "graphql": "^16.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "graphiql": "^3.4.0", + "graphql": "^16.9.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-scripts": "5.0.1" }, "scripts": { @@ -26,7 +26,6 @@ ] }, "devDependencies": { - "eslint-config-react-app": "^5.2.1", - "worker-loader": "^2.0.0" + "eslint-config-react-app": "^7.0.1" } } diff --git a/examples/graphiql-create-react-app/public/index.html b/examples/graphiql-create-react-app/public/index.html index 701a1dbef87..010622e92eb 100644 --- a/examples/graphiql-create-react-app/public/index.html +++ b/examples/graphiql-create-react-app/public/index.html @@ -6,12 +6,11 @@ name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - + - GraphiQL create-react-app Example diff --git a/examples/graphiql-create-react-app/src/App.tsx b/examples/graphiql-create-react-app/src/App.jsx similarity index 62% rename from examples/graphiql-create-react-app/src/App.tsx rename to examples/graphiql-create-react-app/src/App.jsx index 022260f5c67..9175fb03fee 100644 --- a/examples/graphiql-create-react-app/src/App.tsx +++ b/examples/graphiql-create-react-app/src/App.jsx @@ -1,10 +1,8 @@ -import React from 'react'; import { GraphiQL } from 'graphiql'; -import type { Fetcher } from '@graphiql/toolkit'; -import 'graphiql/graphiql.min.css'; +import 'graphiql/graphiql.css'; -const fetcher: Fetcher = async graphQLParams => { - const data = await fetch( +const fetcher = async graphQLParams => { + const response = await fetch( 'https://swapi-graphql.netlify.app/.netlify/functions/index', { method: 'POST', @@ -16,7 +14,7 @@ const fetcher: Fetcher = async graphQLParams => { credentials: 'same-origin', }, ); - return data.json().catch(() => data.text()); + return response.json(); }; const App = () => ; diff --git a/examples/graphiql-create-react-app/src/index.css b/examples/graphiql-create-react-app/src/index.css index d95d5ede303..2b7ec48b007 100644 --- a/examples/graphiql-create-react-app/src/index.css +++ b/examples/graphiql-create-react-app/src/index.css @@ -1,8 +1,6 @@ body { - padding: 0; margin: 0; - min-height: 100vh; } #root { - height: 100vh; + height: 100dvh; } diff --git a/examples/graphiql-create-react-app/src/index.tsx b/examples/graphiql-create-react-app/src/index.jsx similarity index 57% rename from examples/graphiql-create-react-app/src/index.tsx rename to examples/graphiql-create-react-app/src/index.jsx index ac32f49914d..16e6dbd33a2 100644 --- a/examples/graphiql-create-react-app/src/index.tsx +++ b/examples/graphiql-create-react-app/src/index.jsx @@ -1,7 +1,6 @@ -import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; import './index.css'; -const root = createRoot(document.getElementById('root')!); +const root = createRoot(document.getElementById('root')); root.render(); diff --git a/examples/graphiql-create-react-app/src/react-app-env.d.ts b/examples/graphiql-create-react-app/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5fc6b..00000000000 --- a/examples/graphiql-create-react-app/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/graphiql-create-react-app/tsconfig.json b/examples/graphiql-create-react-app/tsconfig.json deleted file mode 100644 index af10394b4c3..00000000000 --- a/examples/graphiql-create-react-app/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react" - }, - "include": ["src"] -} diff --git a/examples/graphiql-parcel/package.json b/examples/graphiql-parcel/package.json index 53b1c11d246..affd3a4dd5d 100644 --- a/examples/graphiql-parcel/package.json +++ b/examples/graphiql-parcel/package.json @@ -1,6 +1,6 @@ { "name": "example-graphiql-parcel", - "version": "1.1.10-alpha.8", + "version": "0.0.0", "license": "MIT", "description": "GraphiQL Parcel Example", "main": "index.js", diff --git a/examples/graphiql-webpack/package.json b/examples/graphiql-webpack/package.json index 8c3bc3c25bf..0559ab93327 100644 --- a/examples/graphiql-webpack/package.json +++ b/examples/graphiql-webpack/package.json @@ -1,6 +1,6 @@ { "name": "example-graphiql-webpack", - "version": "1.1.1-alpha.8", + "version": "0.0.0", "private": true, "license": "MIT", "description": "A GraphiQL example with webpack and typescript", @@ -9,11 +9,11 @@ "start": "NODE_ENV=development webpack-cli serve" }, "dependencies": { - "@graphiql/plugin-code-exporter": "^1.0.4", - "@graphiql/plugin-explorer": "^1.0.3", - "@graphiql/toolkit": "^0.9.1", - "@graphiql/react": "^0.20.3", - "graphiql": "^3.1.1", + "@graphiql/plugin-code-exporter": "^3.1.0", + "@graphiql/plugin-explorer": "^3.2.0", + "@graphiql/toolkit": "^0.10.0", + "@graphiql/react": "^0.24.0", + "graphiql": "^3.5.0", "graphql": "^16.8.1", "graphql-ws": "^5.5.5", "react": "^18.2.0", diff --git a/examples/graphiql-webpack/public/logo.svg b/examples/graphiql-webpack/public/logo.svg index 337843aca18..43e513e06ca 100644 --- a/examples/graphiql-webpack/public/logo.svg +++ b/examples/graphiql-webpack/public/logo.svg @@ -1 +1,27 @@ - + + + + + + + + + diff --git a/examples/graphiql-webpack/src/index.css b/examples/graphiql-webpack/src/index.css index 986c0b8a9ad..179e59ca66e 100644 --- a/examples/graphiql-webpack/src/index.css +++ b/examples/graphiql-webpack/src/index.css @@ -97,10 +97,10 @@ li.select-server--previous-entry button:hover { li.select-server--previous-entry button { background-color: transparent; - border: hsla(var(--color-neutral), 1); + border: hsl(var(--color-neutral)); display: flex; - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); font-size: 1rem; - cursor: pinter; + cursor: pointer; padding: 0.5rem; } diff --git a/examples/graphiql-webpack/src/index.jsx b/examples/graphiql-webpack/src/index.jsx index 455f6540093..ea0adb4c667 100644 --- a/examples/graphiql-webpack/src/index.jsx +++ b/examples/graphiql-webpack/src/index.jsx @@ -84,7 +84,6 @@ const App = () => { return ( ( statusCode: init.status, }; } catch (err) { - // The handler should'nt throw errors. + // The handler shouldn't throw errors. // If you wish to handle them differently, consider implementing your own request handler. console.error( - 'Internal error occurred during request handling. ' + - 'Please check your implementation.', + 'Internal error occurred during request handling. Please check your implementation.', err, ); return { statusCode: 500 }; @@ -57,4 +57,4 @@ export function createHandler( }; } -export const handler = createHandler({ schema }); +export const handler = createHandler({ schema, execute: customExecute }); diff --git a/jest.config.base.js b/jest.config.base.js index 15e87eda8f8..029f7e79290 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -37,13 +37,14 @@ module.exports = (dir, env = 'jsdom') => { testEnvironment: env, testPathIgnorePatterns: ['node_modules', 'dist', 'cypress'], collectCoverageFrom: ['**/src/**/*.{js,jsx,ts,tsx}'], + transformIgnorePatterns: ['node_modules/(!@astrojs/compiler)'], coveragePathIgnorePatterns: [ 'dist', 'esm', 'node_modules', '__tests__', 'resources', - 'test', + 'examples', '.d.ts', 'types.ts', diff --git a/package.json b/package.json index 21399374029..41f402e9534 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,13 @@ }, "lint-staged": { "*.{js,ts,jsx,tsx}": [ - "eslint --cache --fix", - "prettier --write --ignore-path .eslintignore", + "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --cache --fix", + "prettier --write --ignore-path .eslintignore --ignore-path resources/prettierignore", "jest --passWithNoTests", "yarn lint-cspell" ], "*.{md,html,json,css}": [ - "prettier --write --ignore-path .eslintignore", + "prettier --write --ignore-path .eslintignore --ignore-path resources/prettierignore", "yarn lint-cspell" ] }, @@ -29,9 +29,7 @@ "pre-commit": "lint-staged" } }, - "engines": { - "npm": "please_use_yarn_instead" - }, + "packageManager": "yarn@1.22.22", "scripts": { "build": "yarn build-clean && yarn tsc && yarn build:nontsc", "build-bundles": "yarn prebuild-bundles && yarn wsrun:noexamples --stages build-bundles", @@ -43,14 +41,14 @@ "build:watch": "yarn tsc --watch", "build-demo": "wsrun -m build-demo", "watch": "yarn build:watch", - "watch-vscode": "yarn workspace vscode-graphql compile", + "watch-vscode": "yarn tsc && yarn workspace vscode-graphql compile", "watch-vscode-exec": "yarn workspace vscode-graphql-execution compile", "check": "yarn tsc --noEmit", "cypress-open": "yarn workspace graphiql cypress-open", "dev-graphiql": "yarn workspace graphiql dev", "e2e": "yarn run e2e:build && yarn workspace graphiql e2e", "e2e:build": "WEBPACK_SERVE=1 yarn workspace graphiql build-bundles", - "eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --max-warnings=0 --ignore-path .gitignore --cache .", + "eslint": "NODE_OPTIONS=--max-old-space-size=4096 ESLINT_USE_FLAT_CONFIG=false eslint --max-warnings=0 --ignore-path .gitignore --cache .", "format": "yarn eslint --fix && yarn pretty", "jest": "jest", "license-check": "jsgl --local packages/*", @@ -62,7 +60,7 @@ "prepublishOnly": "./scripts/prepublish.sh", "postbuild": "wsrun --exclude-missing postbuild", "pretty": "yarn pretty-check --write", - "pretty-check": "prettier --cache --check --ignore-path .gitignore --ignore-path .eslintignore .", + "pretty-check": "prettier --cache --check --ignore-path .gitignore --ignore-path resources/prettierignore --ignore-path .eslintignore .", "ci:version": "yarn changeset version && yarn build && yarn format", "release": "yarn build && yarn build-bundles && (wsrun release --exclude-missing --serial --recursive --changedSince main -- || true) && yarn changeset publish", "release:canary": "(node scripts/canary-release.js && yarn build-bundles && yarn changeset publish --tag canary) || echo Skipping Canary...", @@ -76,23 +74,27 @@ "test:watch": "yarn jest --watch", "tsc": "tsc --build", "vitest": "yarn wsrun -p -m test", - "wsrun:noexamples": "wsrun --exclude-missing --exclude example-monaco-graphql-react-vite --exclude example-monaco-graphql-nextjs --exclude example-monaco-graphql-webpack --exclude example-graphiql-webpack" + "wsrun:noexamples": "wsrun --exclude-missing --exclude example-monaco-graphql-react-vite --exclude example-monaco-graphql-nextjs --exclude example-monaco-graphql-webpack --exclude example-graphiql-webpack", + "gen-agenda": "wgutils agenda gen" }, "dependencies": { - "@arthurgeron/eslint-plugin-react-usememo": "^1.1.4", + "graphql-http": "^1.22.1", "@babel/cli": "^7.21.0", "@babel/core": "^7.21.0", "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/polyfill": "^7.12.1", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", "@babel/register": "^7.21.0", - "@changesets/changelog-github": "0.4.7", - "@changesets/cli": "2.25.2", + "@changesets/changelog-github": "0.5.0", + "@changesets/cli": "2.27.7", "@manypkg/get-packages": "^1.1.3", - "@shopify/eslint-plugin": "^42.1.0", + "@shopify/eslint-plugin": "^45.0.0", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", "@testing-library/jest-dom": "5.16.5", "@types/codemirror": "^0.0.90", @@ -101,25 +103,26 @@ "@types/jest": "^29.5.2", "@types/node": "^16.18.4", "@types/ws": "8.2.2", - "@typescript-eslint/eslint-plugin": "6.0.0-alpha.159", - "@typescript-eslint/parser": "6.0.0-alpha.159", + "@typescript-eslint/eslint-plugin": "^7.17.0", + "@typescript-eslint/parser": "^7.17.0", "babel-jest": "^29.4.3", + "babel-plugin-transform-import-meta": "^2.2.1", "concurrently": "^7.0.0", "copy": "^0.3.2", "cspell": "^5.15.2", - "eslint": "^8.43.0", - "eslint-config-prettier": "^8.8.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-cypress": "^2.13.3", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^27.2.2", - "eslint-plugin-mdx": "^2.1.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-sonar": "^0.12.0", - "eslint-plugin-sonarjs": "^0.19.0", - "eslint-plugin-unicorn": "^47.0.0", + "eslint": "^9.7.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-cypress": "^3.4.0", + "eslint-plugin-import-x": "^3.1.0", + "eslint-plugin-jest": "^28.6.0", + "eslint-plugin-mdx": "^3.1.5", + "eslint-plugin-promise": "^7.0.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^5.1.0-rc-76002254-20240724", + "eslint-plugin-sonar": "^0.14.1", + "eslint-plugin-sonarjs": "^1.0.4", + "eslint-plugin-unicorn": "^55.0.0", "execa": "^7.1.1", "fetch-mock": "6.5.2", "husky": "^4.2.3", @@ -129,15 +132,19 @@ "mkdirp": "^1.0.4", "patch-package": "^7.0.2", "postinstall-postinstall": "^2.1.0", - "prettier": "3.0.0-alpha.12", + "prettier": "3.3.2", "rimraf": "^3.0.2", "ts-jest": "^27.1.5", "typedoc": "^0.19.2", "typescript": "^4.6.3", - "vitest": "^0.32.2", + "vitest": "^2.0.4", + "wgutils": "^0.1.7", "wsrun": "^5.2.4" }, "resolutions": { - "@babel/traverse": "^7.23.2" + "@babel/traverse": "^7.23.2", + "vscode-languageserver-types": "3.17.3", + "markdown-it": "14.1.0", + "graphql": "17.0.0-alpha.7" } } diff --git a/packages/cm6-graphql/CHANGELOG.md b/packages/cm6-graphql/CHANGELOG.md index fa7947f02f7..3a6cd89d9e7 100644 --- a/packages/cm6-graphql/CHANGELOG.md +++ b/packages/cm6-graphql/CHANGELOG.md @@ -1,5 +1,37 @@ # cm6-graphql +## 0.1.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - graphql-language-service@5.3.0 + +## 0.0.15 + +### Patch Changes + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Fixes several issues with Type System (SDL) completion across the ecosystem: + + - restores completion for object and input type fields when the document context is not detectable or parseable + - correct top-level completions for either of the unknown, type system or executable definitions. this leads to mixed top level completions when the document is unparseable, but now you are not seemingly restricted to only executable top level definitions + - `.graphqls` ad-hoc standard functionality remains, but is not required, as it is not part of the official spec, and the spec also allows mixed mode documents in theory, and this concept is required when the type is unknown + +- Updated dependencies [[`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d)]: + - graphql-language-service@5.2.1 + +## 0.0.14 + +### Patch Changes + +- [#3534](https://github.com/graphql/graphiql/pull/3534) [`f4c98c1f`](https://github.com/graphql/graphiql/commit/f4c98c1f7c6df5a918479e641631e8fbc5b5a92e) Thanks [@johndcollett](https://github.com/johndcollett)! - fix: multiple argument syntax highlighting + ## 0.0.13 ### Patch Changes diff --git a/packages/cm6-graphql/README.md b/packages/cm6-graphql/README.md index a23f718dbff..4ce0fbf2d9a 100644 --- a/packages/cm6-graphql/README.md +++ b/packages/cm6-graphql/README.md @@ -11,7 +11,7 @@ autocomplete and linting powered by your GraphQL Schema. ### Getting Started ```sh -npm install --save cm6-graphql +npm install cm6-graphql ``` [CodeMirror 6](https://codemirror.net/) customization is done through diff --git a/packages/cm6-graphql/__tests__/cases.txt b/packages/cm6-graphql/__tests__/cases.txt index 241700a124d..dbd41900c01 100644 --- a/packages/cm6-graphql/__tests__/cases.txt +++ b/packages/cm6-graphql/__tests__/cases.txt @@ -145,3 +145,13 @@ Document( ) ) ) + +# multiple arguments separated by a commas + +{ + picture(width: 200, height: 100) +} + +==> + +Document(OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,IntValue),Argument(ArgumentAttributeName,IntValue),")"))),"}"))) diff --git a/packages/cm6-graphql/package.json b/packages/cm6-graphql/package.json index 6337050e0fd..1d39db3b16e 100644 --- a/packages/cm6-graphql/package.json +++ b/packages/cm6-graphql/package.json @@ -1,6 +1,6 @@ { "name": "cm6-graphql", - "version": "0.0.13", + "version": "0.1.0", "description": "GraphQL language support for CodeMirror 6", "scripts": { "build": "cm-buildhelper src/index.ts", @@ -16,7 +16,7 @@ "types": "dist/index.d.ts", "sideEffects": false, "dependencies": { - "graphql-language-service": "^5.2.0" + "graphql-language-service": "^5.3.0" }, "devDependencies": { "@codemirror/autocomplete": "6.2.0", @@ -30,7 +30,7 @@ "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.1.0", "esbuild": "0.18.10", - "graphql": "^16.8.1", + "graphql": "^17.0.0-alpha.7", "rollup": "^2.60.2", "rollup-plugin-dts": "^4.0.1", "rollup-plugin-esbuild": "^4.9.1", @@ -44,7 +44,7 @@ "@codemirror/state": "6.2.0", "@codemirror/view": "6.2.1", "@lezer/highlight": "^1.0.0", - "graphql": "^16.5.0" + "graphql": "^16.5.0 || ^17.0.0-alpha.2" }, "license": "MIT" } diff --git a/packages/cm6-graphql/src/completions.ts b/packages/cm6-graphql/src/completions.ts index 27131cf6975..383c69eb7c3 100644 --- a/packages/cm6-graphql/src/completions.ts +++ b/packages/cm6-graphql/src/completions.ts @@ -26,7 +26,14 @@ export const completion = graphqlLanguage.data.of({ } const val = ctx.state.doc.toString(); const pos = offsetToPos(ctx.state.doc, ctx.pos); - const results = getAutocompleteSuggestions(schema, val, pos); + const results = getAutocompleteSuggestions( + schema, + val, + pos, + undefined, + undefined, + opts?.autocompleteOptions, + ); if (results.length === 0) { return null; diff --git a/packages/cm6-graphql/src/interfaces.ts b/packages/cm6-graphql/src/interfaces.ts index f4128928dba..10ab2f2a153 100644 --- a/packages/cm6-graphql/src/interfaces.ts +++ b/packages/cm6-graphql/src/interfaces.ts @@ -1,7 +1,11 @@ import { Completion, CompletionContext } from '@codemirror/autocomplete'; import { EditorView } from '@codemirror/view'; import { GraphQLSchema } from 'graphql'; -import { ContextToken, CompletionItem } from 'graphql-language-service'; +import { + ContextToken, + CompletionItem, + AutocompleteSuggestionOptions, +} from 'graphql-language-service'; import { Position } from './helpers'; export interface GqlExtensionsOptions { showErrorOnInvalidSchema?: boolean; @@ -18,4 +22,5 @@ export interface GqlExtensionsOptions { ctx: CompletionContext, item: Completion, ) => Node | Promise | null; + autocompleteOptions?: AutocompleteSuggestionOptions; } diff --git a/packages/cm6-graphql/src/syntax.grammar b/packages/cm6-graphql/src/syntax.grammar index 71c1c0838aa..1a61dda1d53 100644 --- a/packages/cm6-graphql/src/syntax.grammar +++ b/packages/cm6-graphql/src/syntax.grammar @@ -291,7 +291,7 @@ Directive { DirectiveName Arguments? } Arguments { "(" Argument+ ")"} // https://spec.graphql.org/October2021/#Argument -Argument { ArgumentAttributeName ":" value } +Argument { ArgumentAttributeName ":" value comma? } ArgumentAttributeName { name } diff --git a/packages/codemirror-graphql/CHANGELOG.md b/packages/codemirror-graphql/CHANGELOG.md index a68bc3fbada..3fa9165b6b8 100644 --- a/packages/codemirror-graphql/CHANGELOG.md +++ b/packages/codemirror-graphql/CHANGELOG.md @@ -1,5 +1,46 @@ # Change Log +## 2.1.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - graphql-language-service@5.3.0 + +## 2.0.13 + +### Patch Changes + +- [#3637](https://github.com/graphql/graphiql/pull/3637) [`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - update eslint plugins and fix errors + +- Updated dependencies [[`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d)]: + - graphql-language-service@5.2.2 + +## 2.0.12 + +### Patch Changes + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Fixes several issues with Type System (SDL) completion across the ecosystem: + + - restores completion for object and input type fields when the document context is not detectable or parseable + - correct top-level completions for either of the unknown, type system or executable definitions. this leads to mixed top level completions when the document is unparseable, but now you are not seemingly restricted to only executable top level definitions + - `.graphqls` ad-hoc standard functionality remains, but is not required, as it is not part of the official spec, and the spec also allows mixed mode documents in theory, and this concept is required when the type is unknown + +- Updated dependencies [[`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d)]: + - graphql-language-service@5.2.1 + +## 2.0.11 + +### Patch Changes + +- [#3567](https://github.com/graphql/graphiql/pull/3567) [`fc7de5a7`](https://github.com/graphql/graphiql/commit/fc7de5a75f4b23dd62dd630b705895b5fa5d0a03) Thanks [@retrodaredevil](https://github.com/retrodaredevil)! - tooltip a tag's click listener calls event.preventDefault() to stop navigating away from page + ## 2.0.10 ### Patch Changes diff --git a/packages/codemirror-graphql/README.md b/packages/codemirror-graphql/README.md index ac0ef31d5be..3fa4e98b42d 100644 --- a/packages/codemirror-graphql/README.md +++ b/packages/codemirror-graphql/README.md @@ -15,7 +15,7 @@ typeahead hinter powered by your GraphQL Schema. ### Getting Started ```sh -npm install --save codemirror-graphql +npm install codemirror-graphql ``` CodeMirror helpers install themselves to the global CodeMirror when they are diff --git a/packages/codemirror-graphql/package.json b/packages/codemirror-graphql/package.json index aced6b051e6..41f17330cad 100644 --- a/packages/codemirror-graphql/package.json +++ b/packages/codemirror-graphql/package.json @@ -1,6 +1,6 @@ { "name": "codemirror-graphql", - "version": "2.0.10", + "version": "2.1.0", "description": "GraphQL mode and helpers for CodeMirror.", "contributors": [ "Hyohyeon Jeong ", @@ -40,18 +40,18 @@ "peerDependencies": { "@codemirror/language": "6.0.0", "codemirror": "^5.65.3", - "graphql": "^15.5.0 || ^16.0.0" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2" }, "// TEMPORARILY PINNED until we fix graphql 15 support": "", "dependencies": { "@types/codemirror": "^0.0.90", - "graphql-language-service": "5.2.0" + "graphql-language-service": "5.3.0" }, "devDependencies": { "@codemirror/language": "^6.0.0", "codemirror": "^5.65.3", "cross-env": "^7.0.2", - "graphql": "^16.8.1", + "graphql": "^17.0.0-alpha.7", "rimraf": "^3.0.2", "sane": "2.0.0" } diff --git a/packages/codemirror-graphql/src/__tests__/hint-test.ts b/packages/codemirror-graphql/src/__tests__/hint-test.ts index c71d70a0798..4324ce5eef7 100644 --- a/packages/codemirror-graphql/src/__tests__/hint-test.ts +++ b/packages/codemirror-graphql/src/__tests__/hint-test.ts @@ -32,6 +32,7 @@ import { UnionFirst, UnionSecond, } from './testSchema'; +import { GraphQLDocumentMode } from 'graphql-language-service'; function createEditorWithHint() { return CodeMirror(document.createElement('div'), { @@ -45,7 +46,11 @@ function createEditorWithHint() { }); } -function getHintSuggestions(queryString: string, cursor: CodeMirror.Position) { +function getHintSuggestions( + queryString: string, + cursor: CodeMirror.Position, + opts?: GraphQLHintOptions, +) { const editor = createEditorWithHint(); return new Promise(resolve => { @@ -54,7 +59,7 @@ function getHintSuggestions(queryString: string, cursor: CodeMirror.Position) { cm: CodeMirror.Editor, options: GraphQLHintOptions, ) => { - const result = graphqlHint(cm, options); + const result = graphqlHint(cm, { ...opts, ...options }); resolve(result); CodeMirror.hint.graphql = graphqlHint; return result; @@ -82,14 +87,38 @@ describe('graphql-hint', () => { expect(editor.getHelpers(editor.getCursor(), 'hint')).not.toHaveLength(0); }); - it('provides correct initial keywords', async () => { + it('provides correct initial keywords for executable definitions', async () => { + const suggestions = await getHintSuggestions( + '', + { line: 0, ch: 0 }, + { autocompleteOptions: { mode: GraphQLDocumentMode.EXECUTABLE } }, + ); + const list = [ + { text: 'query' }, + { text: 'mutation' }, + { text: 'subscription' }, + { text: 'fragment' }, + { text: '{' }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct initial keywords for unknown definitions', async () => { const suggestions = await getHintSuggestions('', { line: 0, ch: 0 }); const list = [ + { text: 'extend' }, { text: 'query' }, { text: 'mutation' }, { text: 'subscription' }, { text: 'fragment' }, { text: '{' }, + { text: 'type' }, + { text: 'interface' }, + { text: 'union' }, + { text: 'input' }, + { text: 'scalar' }, + { text: 'schema' }, ]; const expectedSuggestions = getExpectedSuggestions(list); expect(suggestions?.list).toEqual(expectedSuggestions); @@ -946,8 +975,8 @@ describe('graphql-hint', () => { description: 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.', }, - { text: 'TestEnum' }, - { text: 'TestInput' }, + { text: 'TestEnum', description: '' }, + { text: 'TestInput', description: '' }, { text: '__TypeKind', description: @@ -993,8 +1022,8 @@ describe('graphql-hint', () => { description: 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.', }, - { text: 'TestEnum' }, - { text: 'TestInput' }, + { text: 'TestEnum', description: '' }, + { text: 'TestInput', description: '' }, { text: '__TypeKind', description: diff --git a/packages/codemirror-graphql/src/__tests__/lint-test.ts b/packages/codemirror-graphql/src/__tests__/lint-test.ts index 0df6a8a3c9e..e603873dd9b 100644 --- a/packages/codemirror-graphql/src/__tests__/lint-test.ts +++ b/packages/codemirror-graphql/src/__tests__/lint-test.ts @@ -11,7 +11,7 @@ import CodeMirror from 'codemirror'; import 'codemirror/addon/lint/lint'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { GraphQLError, OperationDefinitionNode } from 'graphql'; +import { GraphQLError, OperationDefinitionNode, version } from 'graphql'; import '../lint'; import '../mode'; import { TestSchema } from './testSchema'; @@ -61,7 +61,13 @@ describe('graphql-lint', () => { const noMutationOperationRule = (context: any) => ({ OperationDefinition(node: OperationDefinitionNode) { if (node.operation === 'mutation') { - context.reportError(new GraphQLError('I like turtles.', node)); + context.reportError( + new GraphQLError( + 'I like turtles.', + // @ts-expect-error + parseInt(version, 10) > 16 ? { nodes: node } : node, + ), + ); } return false; }, diff --git a/packages/codemirror-graphql/src/hint.ts b/packages/codemirror-graphql/src/hint.ts index a3a0ae0e308..e3606c94cf1 100644 --- a/packages/codemirror-graphql/src/hint.ts +++ b/packages/codemirror-graphql/src/hint.ts @@ -13,12 +13,16 @@ import CodeMirror, { Hints, Hint } from 'codemirror'; import 'codemirror/addon/hint/show-hint'; import { FragmentDefinitionNode, GraphQLSchema, GraphQLType } from 'graphql'; -import type { Maybe } from 'graphql-language-service'; +import type { + AutocompleteSuggestionOptions, + Maybe, +} from 'graphql-language-service'; import { getAutocompleteSuggestions, Position } from 'graphql-language-service'; export interface GraphQLHintOptions { schema?: GraphQLSchema; externalFragments?: string | FragmentDefinitionNode[]; + autocompleteOptions?: AutocompleteSuggestionOptions; } interface IHint extends Hint { @@ -70,7 +74,7 @@ CodeMirror.registerHelper( editor: CodeMirror.Editor, options: GraphQLHintOptions, ): IHints | undefined => { - const { schema, externalFragments } = options; + const { schema, externalFragments, autocompleteOptions } = options; if (!schema) { return; } @@ -91,11 +95,13 @@ CodeMirror.registerHelper( position, token, externalFragments, + autocompleteOptions, ); const results = { list: rawResults.map(item => ({ - text: item.label, + // important! for when the label is different from the insert text + text: item?.rawInsert ?? item.label, type: item.type, description: item.documentation, isDeprecated: item.isDeprecated, diff --git a/packages/codemirror-graphql/src/info.ts b/packages/codemirror-graphql/src/info.ts index dedea07e170..0e826f6c912 100644 --- a/packages/codemirror-graphql/src/info.ts +++ b/packages/codemirror-graphql/src/info.ts @@ -63,13 +63,13 @@ CodeMirror.registerHelper( } const { kind, step } = token.state; const typeInfo = getTypeInfo(options.schema, token.state); - // Given a Schema and a Token, produce the contents of an info tooltip. // To do this, create a div element that we will render "into" and then pass // it to various rendering functions. if ( (kind === 'Field' && step === 0 && typeInfo.fieldDef) || - (kind === 'AliasedField' && step === 2 && typeInfo.fieldDef) + (kind === 'AliasedField' && step === 2 && typeInfo.fieldDef) || + (kind === 'ObjectField' && step === 0 && typeInfo.fieldDef) ) { const header = document.createElement('div'); header.className = 'CodeMirror-info-header'; @@ -79,6 +79,7 @@ CodeMirror.registerHelper( renderDescription(into, options, typeInfo.fieldDef as any); return into; } + if (kind === 'Directive' && step === 1 && typeInfo.directiveDef) { const header = document.createElement('div'); header.className = 'CodeMirror-info-header'; @@ -97,11 +98,7 @@ CodeMirror.registerHelper( renderDescription(into, options, typeInfo.argDef); return into; } - if ( - kind === 'EnumValue' && - typeInfo.enumValue && - typeInfo.enumValue.description - ) { + if (kind === 'EnumValue' && typeInfo.enumValue?.description) { const header = document.createElement('div'); header.className = 'CodeMirror-info-header'; renderEnumValue(header, typeInfo, options); @@ -299,6 +296,9 @@ function text( // want clicking the node to navigate anywhere. node.href = 'javascript:void 0'; // eslint-disable-line no-script-url node.addEventListener('click', (e: MouseEvent) => { + // Although an href of 'javascript:void 0' should never navigate away from the page, + // that is not always the case: https://github.com/graphql/graphiql/issues/3565 + e.preventDefault(); onClick(ref, e); }); } else { diff --git a/packages/codemirror-graphql/src/utils/getTypeInfo.ts b/packages/codemirror-graphql/src/utils/getTypeInfo.ts index d61d172e126..410cf4db288 100644 --- a/packages/codemirror-graphql/src/utils/getTypeInfo.ts +++ b/packages/codemirror-graphql/src/utils/getTypeInfo.ts @@ -97,11 +97,11 @@ export default function getTypeInfo(schema: GraphQLSchema, tokenState: State) { ? state.prevState.kind === 'Field' ? info.fieldDef : state.prevState.kind === 'Directive' - ? info.directiveDef - : state.prevState.kind === 'AliasedField' - ? state.prevState.name && - getFieldDef(schema, info.parentType, state.prevState.name) - : null + ? info.directiveDef + : state.prevState.kind === 'AliasedField' + ? state.prevState.name && + getFieldDef(schema, info.parentType, state.prevState.name) + : null : null; info.argDefs = parentDef ? (parentDef.args as GraphQLArgument[]) : null; break; @@ -147,6 +147,8 @@ export default function getTypeInfo(schema: GraphQLSchema, tokenState: State) { ? info.objectFieldDefs[state.name] : null; info.inputType = objectField?.type; + // @ts-expect-error + info.fieldDef = objectField; break; case 'NamedType': info.type = state.name ? schema.getType(state.name) : null; diff --git a/packages/codemirror-graphql/src/utils/info-addon.ts b/packages/codemirror-graphql/src/utils/info-addon.ts index 5b017faae8d..cff916607ce 100644 --- a/packages/codemirror-graphql/src/utils/info-addon.ts +++ b/packages/codemirror-graphql/src/utils/info-addon.ts @@ -39,8 +39,8 @@ function createState(options: GraphQLInfoOptions) { options instanceof Function ? { render: options } : options === true - ? {} - : options, + ? {} + : options, }; } diff --git a/packages/codemirror-graphql/src/utils/jump-addon.ts b/packages/codemirror-graphql/src/utils/jump-addon.ts index 0977eb1a903..d3fd3d41f02 100644 --- a/packages/codemirror-graphql/src/utils/jump-addon.ts +++ b/packages/codemirror-graphql/src/utils/jump-addon.ts @@ -121,9 +121,7 @@ function onKeyDown(cm: CodeMirror.Editor, event: KeyboardEvent) { } const isMac = - typeof navigator !== 'undefined' && - navigator && - navigator.appVersion.includes('Mac'); + typeof navigator !== 'undefined' && navigator?.appVersion.includes('Mac'); function isJumpModifier(key: string) { return key === (isMac ? 'Meta' : 'Control'); diff --git a/packages/graphiql-plugin-code-exporter/CHANGELOG.md b/packages/graphiql-plugin-code-exporter/CHANGELOG.md index c16e5829eee..f81f821a28e 100644 --- a/packages/graphiql-plugin-code-exporter/CHANGELOG.md +++ b/packages/graphiql-plugin-code-exporter/CHANGELOG.md @@ -1,5 +1,80 @@ # @graphiql/plugin-code-exporter +## 3.1.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - @graphiql/react@0.24.0 + +## 3.0.5 + +### Patch Changes + +- [#3657](https://github.com/graphql/graphiql/pull/3657) [`5bc7b84`](https://github.com/graphql/graphiql/commit/5bc7b84531b6404553787615d61a5cbcc96c1d6f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - update vite to v5 + +- [#3656](https://github.com/graphql/graphiql/pull/3656) [`93c7e9f`](https://github.com/graphql/graphiql/commit/93c7e9fd224cb4f1e9a86b3391efc1e0ef6e1e3f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - set `build.minify: false` for cjs/esm builds since minified variable names change every build time + +- Updated dependencies [[`5bc7b84`](https://github.com/graphql/graphiql/commit/5bc7b84531b6404553787615d61a5cbcc96c1d6f), [`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d), [`93c7e9f`](https://github.com/graphql/graphiql/commit/93c7e9fd224cb4f1e9a86b3391efc1e0ef6e1e3f)]: + - @graphiql/react@0.23.0 + +## 3.0.4 + +### Patch Changes + +- [#3634](https://github.com/graphql/graphiql/pull/3634) [`adf0ba01`](https://github.com/graphql/graphiql/commit/adf0ba019902dcac2e49ccee69b79a6665c4766d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - when alpha is `1`, use `hsl` instead of `hsla` + +- Updated dependencies [[`adf0ba01`](https://github.com/graphql/graphiql/commit/adf0ba019902dcac2e49ccee69b79a6665c4766d)]: + - @graphiql/react@0.22.4 + +## 3.0.3 + +### Patch Changes + +- Updated dependencies [[`335d830c`](https://github.com/graphql/graphiql/commit/335d830c2a4e551ef97fbeff8ed7c538ff5cd4af)]: + - @graphiql/react@0.22.3 + +## 3.0.2 + +### Patch Changes + +- Updated dependencies [[`03ab3a6b`](https://github.com/graphql/graphiql/commit/03ab3a6b76378591ef79a828d80cc69b0b8f2842)]: + - @graphiql/react@0.22.2 + +## 3.0.1 + +### Patch Changes + +- Updated dependencies [[`224b43f5`](https://github.com/graphql/graphiql/commit/224b43f5473456f264a82998d48a34a441537f54)]: + - @graphiql/react@0.22.1 + +## 3.0.0 + +### Patch Changes + +- Updated dependencies [[`d48f4ef5`](https://github.com/graphql/graphiql/commit/d48f4ef56578dad7ec90f33458353791e463ef7b)]: + - @graphiql/react@0.22.0 + +## 2.0.0 + +### Patch Changes + +- Updated dependencies [[`5d051054`](https://github.com/graphql/graphiql/commit/5d05105469c3f0cbeb5e294da1cf6ff2355e4eb5)]: + - @graphiql/react@0.21.0 + +## 1.0.5 + +### Patch Changes + +- Updated dependencies []: + - @graphiql/react@0.20.4 + ## 1.0.4 ### Patch Changes diff --git a/packages/graphiql-plugin-code-exporter/README.md b/packages/graphiql-plugin-code-exporter/README.md index f7ce70c8948..40764460285 100644 --- a/packages/graphiql-plugin-code-exporter/README.md +++ b/packages/graphiql-plugin-code-exporter/README.md @@ -9,14 +9,14 @@ into the GraphiQL UI. Use your favorite package manager to install the package: ```sh -npm i -S @graphiql/plugin-code-exporter +npm install @graphiql/plugin-code-exporter ``` The following packages are peer dependencies, so make sure you have them installed as well: ```sh -npm i -S react react-dom graphql +npm install react react-dom graphql ``` ## Usage @@ -27,11 +27,10 @@ for all details on available `props` and how to [create snippets](https://github.com/OneGraph/graphiql-code-exporter#snippets). ```jsx -import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; -import { createGraphiQLFetcher } from '@graphiql/toolkit'; -import { GraphiQL } from 'graphiql'; import { useState } from 'react'; - +import { GraphiQL } from 'graphiql'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; import 'graphiql/graphiql.css'; import '@graphiql/plugin-code-exporter/dist/style.css'; @@ -39,10 +38,10 @@ const fetcher = createGraphiQLFetcher({ url: 'https://swapi-graphql.netlify.app/.netlify/functions/index', }); -/* -Example code for snippets. See https://github.com/OneGraph/graphiql-code-exporter#snippets for details -*/ - +/** + * Example code for snippets. See https://github.com/OneGraph/graphiql-code-exporter#snippets for + * details + */ const removeQueryName = query => query.replace( /^[^{(]+([{(])/, @@ -91,7 +90,7 @@ const exporter = codeExporterPlugin({ codeMirrorTheme: 'graphiql', }); -function GraphiQLWithExplorer() { +function GraphiQLWithCodeExporter() { return ( ); diff --git a/packages/graphiql-plugin-code-exporter/examples/index.html b/packages/graphiql-plugin-code-exporter/examples/index.html index d4dce712ec5..c1929dedd43 100644 --- a/packages/graphiql-plugin-code-exporter/examples/index.html +++ b/packages/graphiql-plugin-code-exporter/examples/index.html @@ -76,9 +76,9 @@ } var exampleSnippetOne = { - name: `Example One`, - language: `JavaScript`, - codeMirrorMode: `jsx`, + name: 'Example One', + language: 'JavaScript', + codeMirrorMode: 'jsx', options: [], generate: arg => `export const query = graphql\` ${getQuery(arg, 2)} @@ -87,9 +87,9 @@ }; var exampleSnippetTwo = { - name: `Example Two`, - language: `JavaScript`, - codeMirrorMode: `jsx`, + name: 'Example Two', + language: 'JavaScript', + codeMirrorMode: 'jsx', options: [], generate: arg => `import { graphql } from 'graphql' @@ -107,7 +107,7 @@ var query = 'query AllFilms {\n allFilms {\n films {\n title\n }\n }\n}'; - function GraphiQLWithExporter() { + function GraphiQLWithCodeExporter() { return React.createElement(GraphiQL, { fetcher: fetcher, defaultEditorToolsVisibility: true, @@ -117,7 +117,7 @@ } const root = ReactDOM.createRoot(document.getElementById('graphiql')); - root.render(React.createElement(GraphiQLWithExporter)); + root.render(React.createElement(GraphiQLWithCodeExporter)); diff --git a/packages/graphiql-plugin-code-exporter/package.json b/packages/graphiql-plugin-code-exporter/package.json index 931c3db90aa..d6191c0965e 100644 --- a/packages/graphiql-plugin-code-exporter/package.json +++ b/packages/graphiql-plugin-code-exporter/package.json @@ -1,6 +1,6 @@ { "name": "@graphiql/plugin-code-exporter", - "version": "1.0.4", + "version": "3.1.0", "repository": { "type": "git", "url": "https://github.com/graphql/graphiql", @@ -33,19 +33,19 @@ "graphiql-code-exporter": "^3.0.3" }, "peerDependencies": { - "@graphiql/react": "^0.20.3", - "graphql": "^15.5.0 || ^16.0.0", + "@graphiql/react": "^0.24.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2", "react": "^16.8.0 || ^17 || ^18", "react-dom": "^16.8.0 || ^17 || ^18" }, "devDependencies": { - "@graphiql/react": "^0.20.3", - "@vitejs/plugin-react": "^4.0.1", - "graphql": "^16.8.1", + "@graphiql/react": "^0.24.0", + "@vitejs/plugin-react": "^4.3.1", + "graphql": "^17.0.0-alpha.7", "postcss-nesting": "^10.1.7", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^4.6.3", - "vite": "^4.3.9" + "vite": "^5.3.5" } } diff --git a/packages/graphiql-plugin-code-exporter/src/index.css b/packages/graphiql-plugin-code-exporter/src/index.css index a4b88eaccf8..716d01d954c 100644 --- a/packages/graphiql-plugin-code-exporter/src/index.css +++ b/packages/graphiql-plugin-code-exporter/src/index.css @@ -82,7 +82,7 @@ cursor: pointer; text-decoration: none; padding: var(--px-8) var(--px-12); - color: hsla(var(--color-neutral), 1) !important; + color: hsl(var(--color-neutral)) !important; border-radius: var(--border-radius-4) !important; &:hover { background-color: hsla( diff --git a/packages/graphiql-plugin-code-exporter/vite.config.ts b/packages/graphiql-plugin-code-exporter/vite.config.mts similarity index 96% rename from packages/graphiql-plugin-code-exporter/vite.config.ts rename to packages/graphiql-plugin-code-exporter/vite.config.mts index 7db8ba68dcc..b963a34933f 100644 --- a/packages/graphiql-plugin-code-exporter/vite.config.ts +++ b/packages/graphiql-plugin-code-exporter/vite.config.mts @@ -7,6 +7,7 @@ const IS_UMD = process.env.UMD === 'true'; export default defineConfig({ plugins: [react({ jsxRuntime: 'classic' })], build: { + minify: IS_UMD ? 'esbuild' : false, // avoid clean cjs/es builds emptyOutDir: !IS_UMD, lib: { diff --git a/packages/graphiql-plugin-explorer/CHANGELOG.md b/packages/graphiql-plugin-explorer/CHANGELOG.md index 3060ce25c69..8f5abee185e 100644 --- a/packages/graphiql-plugin-explorer/CHANGELOG.md +++ b/packages/graphiql-plugin-explorer/CHANGELOG.md @@ -1,5 +1,82 @@ # @graphiql/plugin-explorer +## 3.2.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - @graphiql/react@0.24.0 + +## 3.1.1 + +### Patch Changes + +- [#3657](https://github.com/graphql/graphiql/pull/3657) [`5bc7b84`](https://github.com/graphql/graphiql/commit/5bc7b84531b6404553787615d61a5cbcc96c1d6f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - update vite to v5 + +- [#3656](https://github.com/graphql/graphiql/pull/3656) [`93c7e9f`](https://github.com/graphql/graphiql/commit/93c7e9fd224cb4f1e9a86b3391efc1e0ef6e1e3f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - set `build.minify: false` for cjs/esm builds since minified variable names change every build time + +- Updated dependencies [[`5bc7b84`](https://github.com/graphql/graphiql/commit/5bc7b84531b6404553787615d61a5cbcc96c1d6f), [`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d), [`93c7e9f`](https://github.com/graphql/graphiql/commit/93c7e9fd224cb4f1e9a86b3391efc1e0ef6e1e3f)]: + - @graphiql/react@0.23.0 + +## 3.1.0 + +### Minor Changes + +- [#3633](https://github.com/graphql/graphiql/pull/3633) [`8849a15b`](https://github.com/graphql/graphiql/commit/8849a15b6e80fe1b34e8250e74a56b85ccdb6ac6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - adjust `@graphiql/plugin-explorer` styles + +### Patch Changes + +- Updated dependencies [[`adf0ba01`](https://github.com/graphql/graphiql/commit/adf0ba019902dcac2e49ccee69b79a6665c4766d)]: + - @graphiql/react@0.22.4 + +## 3.0.3 + +### Patch Changes + +- Updated dependencies [[`335d830c`](https://github.com/graphql/graphiql/commit/335d830c2a4e551ef97fbeff8ed7c538ff5cd4af)]: + - @graphiql/react@0.22.3 + +## 3.0.2 + +### Patch Changes + +- Updated dependencies [[`03ab3a6b`](https://github.com/graphql/graphiql/commit/03ab3a6b76378591ef79a828d80cc69b0b8f2842)]: + - @graphiql/react@0.22.2 + +## 3.0.1 + +### Patch Changes + +- Updated dependencies [[`224b43f5`](https://github.com/graphql/graphiql/commit/224b43f5473456f264a82998d48a34a441537f54)]: + - @graphiql/react@0.22.1 + +## 3.0.0 + +### Patch Changes + +- Updated dependencies [[`d48f4ef5`](https://github.com/graphql/graphiql/commit/d48f4ef56578dad7ec90f33458353791e463ef7b)]: + - @graphiql/react@0.22.0 + +## 2.0.0 + +### Patch Changes + +- Updated dependencies [[`5d051054`](https://github.com/graphql/graphiql/commit/5d05105469c3f0cbeb5e294da1cf6ff2355e4eb5)]: + - @graphiql/react@0.21.0 + +## 1.0.4 + +### Patch Changes + +- Updated dependencies []: + - @graphiql/react@0.20.4 + ## 1.0.3 ### Patch Changes diff --git a/packages/graphiql-plugin-explorer/README.md b/packages/graphiql-plugin-explorer/README.md index 232d8fd1554..69bcbad7eb6 100644 --- a/packages/graphiql-plugin-explorer/README.md +++ b/packages/graphiql-plugin-explorer/README.md @@ -9,24 +9,22 @@ GraphiQL UI. Use your favorite package manager to install the package: ```sh -npm i -S @graphiql/plugin-explorer +npm install @graphiql/plugin-explorer ``` The following packages are peer dependencies, so make sure you have them installed as well: ```sh -npm i -S react react-dom graphql +npm install react react-dom graphql ``` ## Usage ```jsx -import { explorerPlugin } from '@graphiql/plugin-explorer'; -import { createGraphiQLFetcher } from '@graphiql/toolkit'; import { GraphiQL } from 'graphiql'; -import { useState } from 'react'; - +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { explorerPlugin } from '@graphiql/plugin-explorer'; import 'graphiql/graphiql.css'; import '@graphiql/plugin-explorer/dist/style.css'; @@ -34,10 +32,12 @@ const fetcher = createGraphiQLFetcher({ url: 'https://swapi-graphql.netlify.app/.netlify/functions/index', }); -// pass the explorer props here if you want +// Pass the explorer props here if you want const explorer = explorerPlugin(); -return ; +function GraphiQLWithExplorer() { + return ; +} ``` ## CDN bundles diff --git a/packages/graphiql-plugin-explorer/package.json b/packages/graphiql-plugin-explorer/package.json index c2d86c8eccd..b8bd6c6763e 100644 --- a/packages/graphiql-plugin-explorer/package.json +++ b/packages/graphiql-plugin-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@graphiql/plugin-explorer", - "version": "1.0.3", + "version": "3.2.0", "repository": { "type": "git", "url": "https://github.com/graphql/graphiql", @@ -32,18 +32,19 @@ "graphiql-explorer": "^0.9.0" }, "peerDependencies": { - "@graphiql/react": "^0.20.3", - "graphql": "^15.5.0 || ^16.0.0", + "@graphiql/react": "^0.24.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2", "react": "^16.8.0 || ^17 || ^18", "react-dom": "^16.8.0 || ^17 || ^18" }, "devDependencies": { - "@graphiql/react": "^0.20.3", - "@vitejs/plugin-react": "^4.0.1", - "graphql": "^16.8.1", + "@graphiql/react": "^0.24.0", + "@vitejs/plugin-react": "^4.3.1", + "graphql": "^17.0.0-alpha.7", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^4.6.3", - "vite": "^4.3.9" + "vite": "^5.3.5", + "vite-plugin-svgr": "^4.2.0" } } diff --git a/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts b/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts index 66fffce659a..089b3331a0f 100644 --- a/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts +++ b/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts @@ -42,7 +42,7 @@ declare module 'graphiql-explorer' { string2: string; variable: string; atom: string; - } | null; + }; arrowOpen?: ReactNode; arrowClosed?: ReactNode; checkboxChecked?: ReactNode; @@ -51,8 +51,8 @@ declare module 'graphiql-explorer' { explorerActionsStyle?: CSSProperties; buttonStyle?: CSSProperties; actionButtonStyle?: CSSProperties; - } | null; - showAttribution: boolean; + }; + showAttribution?: boolean; hideActions?: boolean; externalFragments?: FragmentDefinitionNode[]; }; diff --git a/packages/graphiql-plugin-explorer/src/icons/arrow.svg b/packages/graphiql-plugin-explorer/src/icons/arrow.svg new file mode 100644 index 00000000000..245d49ea178 --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/icons/arrow.svg @@ -0,0 +1,11 @@ + + + diff --git a/packages/graphiql-plugin-explorer/src/icons/checkbox-checked.svg b/packages/graphiql-plugin-explorer/src/icons/checkbox-checked.svg new file mode 100644 index 00000000000..c77bb4d2447 --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/icons/checkbox-checked.svg @@ -0,0 +1,14 @@ + + + + diff --git a/packages/graphiql-plugin-explorer/src/icons/checkbox-unchecked.svg b/packages/graphiql-plugin-explorer/src/icons/checkbox-unchecked.svg new file mode 100644 index 00000000000..64652f54408 --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/icons/checkbox-unchecked.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/graphiql-plugin-explorer/src/icons/folder-plus.svg b/packages/graphiql-plugin-explorer/src/icons/folder-plus.svg new file mode 100644 index 00000000000..d6714651b81 --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/icons/folder-plus.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/graphiql-plugin-explorer/src/index.css b/packages/graphiql-plugin-explorer/src/index.css index 96e578497e8..0c096caf793 100644 --- a/packages/graphiql-plugin-explorer/src/index.css +++ b/packages/graphiql-plugin-explorer/src/index.css @@ -4,6 +4,10 @@ width: unset !important; } +.docExplorerWrap svg { + display: unset; +} + .doc-explorer-title { font-size: var(--font-size-h2); font-weight: var(--font-weight-medium); @@ -19,20 +23,42 @@ padding: 0 !important; } -.graphiql-explorer-root > div:first-child { - padding-left: var(--px-8); - overflow: hidden !important; +.graphiql-explorer-root > div > div { + border-color: hsla( + var(--color-neutral), + var(--alpha-background-heavy) + ) !important; + padding-top: var(--px-16); } .graphiql-explorer-root input { - background: hsl(var(--color-base)); + background: unset; } .graphiql-explorer-root select { - background-color: hsl(var(--color-base)); + background: hsl(var(--color-base)) !important; border: 1px solid hsla(var(--color-neutral), var(--alpha-secondary)); border-radius: var(--border-radius-4); - color: hsl(var(--color-neutral)); - margin: 0 var(--px-4); + color: hsl(var(--color-neutral)) !important; + margin: 0 var(--px-8); padding: var(--px-4) var(--px-6); } + +.graphiql-operation-title-bar .toolbar-button { + line-height: 0; + margin-left: var(--px-8); + color: hsla(var(--color-neutral), var(--alpha-secondary, 0.6)); + font-size: var(--font-size-h3); + vertical-align: middle; +} + +.graphiql-explorer-graphql-arguments input { + line-height: 0; +} + +.graphiql-explorer-actions { + border-color: hsla( + var(--color-neutral), + var(--alpha-background-heavy) + ) !important; +} diff --git a/packages/graphiql-plugin-explorer/src/index.tsx b/packages/graphiql-plugin-explorer/src/index.tsx index c7b4bb625fd..d4990c2aada 100644 --- a/packages/graphiql-plugin-explorer/src/index.tsx +++ b/packages/graphiql-plugin-explorer/src/index.tsx @@ -1,3 +1,4 @@ +import React, { CSSProperties, useCallback } from 'react'; import { GraphiQLPlugin, useEditorContext, @@ -10,7 +11,11 @@ import { Explorer as GraphiQLExplorer, GraphiQLExplorerProps, } from 'graphiql-explorer'; -import React, { useCallback } from 'react'; + +import ArrowIcon from './icons/arrow.svg?react'; +import FolderPlusIcon from './icons/folder-plus.svg?react'; +import CheckboxUncheckedIcon from './icons/checkbox-unchecked.svg?react'; +import CheckboxCheckedIcon from './icons/checkbox-checked.svg?react'; import './graphiql-explorer.d.ts'; import './index.css'; @@ -30,91 +35,28 @@ const colors = { }; const arrowOpen = ( - - - -); - -const arrowClosed = ( - - - + ); - +const arrowClosed = ; const checkboxUnchecked = ( - - - + ); - const checkboxChecked = ( - - - - + ); -const styles = { +const styles: Record = { buttonStyle: { - backgroundColor: 'transparent', - border: 'none', - color: 'hsla(var(--color-neutral), var(--alpha-secondary, 0.6))', cursor: 'pointer', - fontSize: '1em', + fontSize: '2em', + lineHeight: 0, }, explorerActionsStyle: { - padding: 'var(--px-8) var(--px-4)', - }, - actionButtonStyle: { - backgroundColor: 'transparent', - border: 'none', - color: 'hsla(var(--color-neutral), var(--alpha-secondary, 0.6))', - cursor: 'pointer', - fontSize: '1em', + paddingTop: 'var(--px-16)', }, + actionButtonStyle: {}, }; export type GraphiQLExplorerPluginProps = Omit< @@ -163,32 +105,11 @@ function ExplorerPlugin(props: GraphiQLExplorerPluginProps) { } export function explorerPlugin( - props: GraphiQLExplorerPluginProps, + props?: GraphiQLExplorerPluginProps, ): GraphiQLPlugin { return { title: 'GraphiQL Explorer', - icon: () => ( - - - - - - ), + icon: FolderPlusIcon, content: () => , }; } diff --git a/packages/graphiql-plugin-explorer/src/vite-env.d.ts b/packages/graphiql-plugin-explorer/src/vite-env.d.ts index 11f02fe2a00..ee9857a625d 100644 --- a/packages/graphiql-plugin-explorer/src/vite-env.d.ts +++ b/packages/graphiql-plugin-explorer/src/vite-env.d.ts @@ -1 +1,2 @@ +/// /// diff --git a/packages/graphiql-plugin-explorer/vite.config.ts b/packages/graphiql-plugin-explorer/vite.config.mts similarity index 81% rename from packages/graphiql-plugin-explorer/vite.config.ts rename to packages/graphiql-plugin-explorer/vite.config.mts index cd4a63c8c99..e47ea748b70 100644 --- a/packages/graphiql-plugin-explorer/vite.config.ts +++ b/packages/graphiql-plugin-explorer/vite.config.mts @@ -1,12 +1,22 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; import packageJSON from './package.json'; const IS_UMD = process.env.UMD === 'true'; export default defineConfig({ - plugins: [react({ jsxRuntime: 'classic' })], + plugins: [ + react({ jsxRuntime: 'classic' }), + svgr({ + exportAsDefault: true, + svgrOptions: { + titleProp: true, + }, + }), + ], build: { + minify: IS_UMD ? 'esbuild' : false, // avoid clean cjs/es builds emptyOutDir: !IS_UMD, lib: { diff --git a/packages/graphiql-react/CHANGELOG.md b/packages/graphiql-react/CHANGELOG.md index bb8e2d91838..2335f77e639 100644 --- a/packages/graphiql-react/CHANGELOG.md +++ b/packages/graphiql-react/CHANGELOG.md @@ -1,5 +1,90 @@ # @graphiql/react +## 0.24.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - graphql-language-service@5.3.0 + - codemirror-graphql@2.1.0 + - @graphiql/toolkit@0.10.0 + +## 0.23.1 + +### Patch Changes + +- [#3552](https://github.com/graphql/graphiql/pull/3552) [`6a0a5e5`](https://github.com/graphql/graphiql/commit/6a0a5e590b7b526af8a66c59a27ec3d0144af572) Thanks [@klippx](https://github.com/klippx)! - do not clear `defaultHeaders` when switching between tabs upon reload + +## 0.23.0 + +### Minor Changes + +- [#3657](https://github.com/graphql/graphiql/pull/3657) [`5bc7b84`](https://github.com/graphql/graphiql/commit/5bc7b84531b6404553787615d61a5cbcc96c1d6f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - update vite to v5 + +### Patch Changes + +- [#3637](https://github.com/graphql/graphiql/pull/3637) [`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - update eslint plugins and fix errors + +- [#3656](https://github.com/graphql/graphiql/pull/3656) [`93c7e9f`](https://github.com/graphql/graphiql/commit/93c7e9fd224cb4f1e9a86b3391efc1e0ef6e1e3f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - set `build.minify: false` for cjs/esm builds since minified variable names change every build time + +- Updated dependencies [[`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d), [`56c6f45`](https://github.com/graphql/graphiql/commit/56c6f4571dd0dfda307ed11c5afb8c837ad928b0)]: + - codemirror-graphql@2.0.13 + - graphql-language-service@5.2.2 + - @graphiql/toolkit@0.9.2 + +## 0.22.4 + +### Patch Changes + +- [#3634](https://github.com/graphql/graphiql/pull/3634) [`adf0ba01`](https://github.com/graphql/graphiql/commit/adf0ba019902dcac2e49ccee69b79a6665c4766d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - when alpha is `1`, use `hsl` instead of `hsla` + +## 0.22.3 + +### Patch Changes + +- [#3624](https://github.com/graphql/graphiql/pull/3624) [`335d830c`](https://github.com/graphql/graphiql/commit/335d830c2a4e551ef97fbeff8ed7c538ff5cd4af) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix doc explorer search input is cut off while clicking on autocomplete results + +## 0.22.2 + +### Patch Changes + +- [#3602](https://github.com/graphql/graphiql/pull/3602) [`03ab3a6b`](https://github.com/graphql/graphiql/commit/03ab3a6b76378591ef79a828d80cc69b0b8f2842) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Avoid using deprecated Component.defaultProps for icon titles + +- Updated dependencies [[`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d)]: + - graphql-language-service@5.2.1 + - codemirror-graphql@2.0.12 + +## 0.22.1 + +### Patch Changes + +- [#3597](https://github.com/graphql/graphiql/pull/3597) [`224b43f5`](https://github.com/graphql/graphiql/commit/224b43f5473456f264a82998d48a34a441537f54) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix TypeScript type of the `label` prop of the `Tooltip` component + +## 0.22.0 + +### Minor Changes + +- [#3580](https://github.com/graphql/graphiql/pull/3580) [`d48f4ef5`](https://github.com/graphql/graphiql/commit/d48f4ef56578dad7ec90f33458353791e463ef7b) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Implement correct merging of incremental responses (@defer/@stream) + +## 0.21.0 + +### Minor Changes + +- [#3569](https://github.com/graphql/graphiql/pull/3569) [`5d051054`](https://github.com/graphql/graphiql/commit/5d05105469c3f0cbeb5e294da1cf6ff2355e4eb5) Thanks [@AaronMoat](https://github.com/AaronMoat)! - Update to markdown-it 14.x + +## 0.20.4 + +### Patch Changes + +- Updated dependencies [[`fc7de5a7`](https://github.com/graphql/graphiql/commit/fc7de5a75f4b23dd62dd630b705895b5fa5d0a03)]: + - codemirror-graphql@2.0.11 + ## 0.20.3 ### Patch Changes diff --git a/packages/graphiql-react/jest.config.js b/packages/graphiql-react/jest.config.js index 22b3f1af305..4b9d01d0279 100644 --- a/packages/graphiql-react/jest.config.js +++ b/packages/graphiql-react/jest.config.js @@ -3,7 +3,7 @@ const base = require('../../jest.config.base')(__dirname); module.exports = { ...base, moduleNameMapper: { - '\\.svg$': `${__dirname}/__mocks__/svg`, + '\\.svg\\?react$': `${__dirname}/__mocks__/svg`, ...base.moduleNameMapper, }, }; diff --git a/packages/graphiql-react/package.json b/packages/graphiql-react/package.json index 7d48807a3a9..2c293b04769 100644 --- a/packages/graphiql-react/package.json +++ b/packages/graphiql-react/package.json @@ -1,6 +1,6 @@ { "name": "@graphiql/react", - "version": "0.20.3", + "version": "0.24.0", "repository": { "type": "git", "url": "https://github.com/graphql/graphiql", @@ -43,12 +43,12 @@ "build": "tsc --emitDeclarationOnly && vite build" }, "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2", "react": "^16.8.0 || ^17 || ^18", "react-dom": "^16.8.0 || ^17 || ^18" }, "dependencies": { - "@graphiql/toolkit": "^0.9.1", + "@graphiql/toolkit": "^0.10.0", "@headlessui/react": "^1.7.15", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", @@ -57,24 +57,27 @@ "@types/codemirror": "^5.60.8", "clsx": "^1.2.1", "codemirror": "^5.65.3", - "codemirror-graphql": "^2.0.10", + "codemirror-graphql": "^2.1.0", "copy-to-clipboard": "^3.2.0", "framer-motion": "^6.5.1", - "graphql-language-service": "^5.2.0", - "markdown-it": "^12.2.0", + "get-value": "^3.0.1", + "graphql-language-service": "^5.3.0", + "markdown-it": "^14.1.0", "set-value": "^4.1.0" }, "devDependencies": { "@babel/helper-string-parser": "^7.19.4", "@testing-library/react": "14.0.0", + "@types/markdown-it": "^14.1.2", + "@types/get-value": "^3.0.5", "@types/set-value": "^4.0.1", - "@vitejs/plugin-react": "^4.0.1", - "graphql": "^16.8.1", + "@vitejs/plugin-react": "^4.3.1", + "graphql": "^17.0.0-alpha.7", "postcss-nesting": "^10.1.7", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^4.6.3", - "vite": "^4.3.9", - "vite-plugin-svgr": "^3.2.0" + "vite": "^5.3.5", + "vite-plugin-svgr": "^4.2.0" } } diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 26f4d232459..b592dd680da 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -360,13 +360,14 @@ export function EditorContextProvider(props: EditorContextProviderProps) { headerEditor, responseEditor, }); + const { onTabChange, defaultHeaders, children } = props; const setEditorValues = useSetEditorValues({ queryEditor, variableEditor, headerEditor, responseEditor, + defaultHeaders, }); - const { onTabChange, defaultHeaders, children } = props; const addTab = useCallback(() => { setTabState(current => { diff --git a/packages/graphiql-react/src/editor/query-editor.ts b/packages/graphiql-react/src/editor/query-editor.ts index 33147427dd2..48c8aec4f97 100644 --- a/packages/graphiql-react/src/editor/query-editor.ts +++ b/packages/graphiql-react/src/editor/query-editor.ts @@ -6,7 +6,10 @@ import type { GraphQLSchema, ValidationRule, } from 'graphql'; -import { getOperationFacts } from 'graphql-language-service'; +import { + getOperationFacts, + GraphQLDocumentMode, +} from 'graphql-language-service'; import { MutableRefObject, useCallback, @@ -186,6 +189,10 @@ export function useQueryEditor( completeSingle: false, container, externalFragments: undefined, + autocompleteOptions: { + // for the query editor, restrict to executable type definitions + mode: GraphQLDocumentMode.EXECUTABLE, + }, }, info: { schema: undefined, diff --git a/packages/graphiql-react/src/editor/response-editor.tsx b/packages/graphiql-react/src/editor/response-editor.tsx index c7b7cb31b75..03f1d7e069c 100644 --- a/packages/graphiql-react/src/editor/response-editor.tsx +++ b/packages/graphiql-react/src/editor/response-editor.tsx @@ -105,9 +105,11 @@ export function useResponseEditor( // We can't refactor to root.unmount() from React 18 because we support React 16/17 too if (!infoElements.length) { + // eslint-disable-next-line react/no-deprecated -- We still support React 16/17 ReactDOM.unmountComponentAtNode(tooltipDiv); return null; } + // eslint-disable-next-line react/no-deprecated -- We still support React 16/17 ReactDOM.render(infoElements, tooltipDiv); return tooltipDiv; }, diff --git a/packages/graphiql-react/src/editor/style/info.css b/packages/graphiql-react/src/editor/style/info.css index 82a8414326e..a1a84335c92 100644 --- a/packages/graphiql-react/src/editor/style/info.css +++ b/packages/graphiql-react/src/editor/style/info.css @@ -4,7 +4,7 @@ border: var(--popover-border); border-radius: var(--border-radius-8); box-shadow: var(--popover-box-shadow); - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); max-height: 300px; max-width: 400px; opacity: 0; diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index b9110dd5135..067d730666b 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -258,11 +258,13 @@ export function useSetEditorValues({ variableEditor, headerEditor, responseEditor, + defaultHeaders, }: { queryEditor: CodeMirrorEditorWithOperationFacts | null; variableEditor: CodeMirrorEditor | null; headerEditor: CodeMirrorEditor | null; responseEditor: CodeMirrorEditor | null; + defaultHeaders?: string; }) { return useCallback( ({ @@ -278,10 +280,10 @@ export function useSetEditorValues({ }) => { queryEditor?.setValue(query ?? ''); variableEditor?.setValue(variables ?? ''); - headerEditor?.setValue(headers ?? ''); + headerEditor?.setValue(headers ?? defaultHeaders ?? ''); responseEditor?.setValue(response ?? ''); }, - [headerEditor, queryEditor, responseEditor, variableEditor], + [headerEditor, queryEditor, responseEditor, variableEditor, defaultHeaders], ); } diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/execution.tsx index d66b4eea78b..5df69e39bbf 100644 --- a/packages/graphiql-react/src/execution.tsx +++ b/packages/graphiql-react/src/execution.tsx @@ -1,16 +1,21 @@ import { Fetcher, - FetcherResultPayload, formatError, formatResult, isAsyncIterable, isObservable, Unsubscribable, } from '@graphiql/toolkit'; -import { ExecutionResult, FragmentDefinitionNode, print } from 'graphql'; +import { + ExecutionResult, + FragmentDefinitionNode, + GraphQLError, + print, +} from 'graphql'; import { getFragmentDependenciesForAST } from 'graphql-language-service'; import { ReactNode, useCallback, useMemo, useRef, useState } from 'react'; import setValue from 'set-value'; +import getValue from 'get-value'; import { useAutoCompleteLeafs, useEditorContext } from './editor'; import { UseAutoCompleteLeafsArgs } from './editor/hooks'; @@ -183,7 +188,7 @@ export function ExecutionContextProvider({ }); try { - let fullResponse: FetcherResultPayload = { data: {} }; + const fullResponse: ExecutionResult = {}; const handleResponse = (result: ExecutionResult) => { // A different query was dispatched in the meantime, so don't // show the results of this one. @@ -202,40 +207,8 @@ export function ExecutionContextProvider({ } if (maybeMultipart) { - const payload: FetcherResultPayload = { - data: fullResponse.data, - }; - const maybeErrors = [ - ...(fullResponse?.errors || []), - ...maybeMultipart.flatMap(i => i.errors).filter(Boolean), - ]; - - if (maybeErrors.length) { - payload.errors = maybeErrors; - } - for (const part of maybeMultipart) { - // We pull out errors here, so we dont include it later - const { path, data, errors, ...rest } = part; - if (path) { - if (!data) { - throw new Error( - `Expected part to contain a data property, but got ${part}`, - ); - } - - setValue(payload.data, path, data, { merge: true }); - } else if (data) { - // If there is no path, we don't know what to do with the payload, - // so we just set it. - payload.data = data; - } - - // Ensures we also bring extensions and alike along for the ride - fullResponse = { - ...payload, - ...rest, - }; + mergeIncrementalResult(fullResponse, part); } setIsFetching(false); @@ -361,3 +334,127 @@ function tryParseJsonObject({ } return parsed; } + +type IncrementalResult = { + data?: Record | null; + errors?: ReadonlyArray; + extensions?: Record; + hasNext?: boolean; + path?: ReadonlyArray; + incremental?: ReadonlyArray; + label?: string; + items?: ReadonlyArray> | null; + pending?: ReadonlyArray<{ id: string; path: ReadonlyArray }>; + completed?: ReadonlyArray<{ + id: string; + errors?: ReadonlyArray; + }>; + id?: string; + subPath?: ReadonlyArray; +}; + +const pathsMap = new WeakMap< + ExecutionResult, + Map> +>(); + +/** + * @param executionResult The complete execution result object which will be + * mutated by merging the contents of the incremental result. + * @param incrementalResult The incremental result that will be merged into the + * complete execution result. + */ +function mergeIncrementalResult( + executionResult: IncrementalResult, + incrementalResult: IncrementalResult, +): void { + let path: ReadonlyArray | undefined = [ + 'data', + ...(incrementalResult.path ?? []), + ]; + + for (const result of [executionResult, incrementalResult]) { + if (result.pending) { + let paths = pathsMap.get(executionResult); + if (paths === undefined) { + paths = new Map(); + pathsMap.set(executionResult, paths); + } + + for (const { id, path: pendingPath } of result.pending) { + paths.set(id, ['data', ...pendingPath]); + } + } + } + + const { items } = incrementalResult; + if (items) { + const { id } = incrementalResult; + if (id) { + path = pathsMap.get(executionResult)?.get(id); + if (path === undefined) { + throw new Error('Invalid incremental delivery format.'); + } + + const list = getValue(executionResult, path.join('.')); + list.push(...items); + } else { + path = ['data', ...(incrementalResult.path ?? [])]; + for (const item of items) { + setValue(executionResult, path.join('.'), item); + // Increment the last path segment (the array index) to merge the next item at the next index + // eslint-disable-next-line unicorn/prefer-at -- cannot mutate the array using Array.at() + (path[path.length - 1] as number)++; + } + } + } + + const { data } = incrementalResult; + if (data) { + const { id } = incrementalResult; + if (id) { + path = pathsMap.get(executionResult)?.get(id); + if (path === undefined) { + throw new Error('Invalid incremental delivery format.'); + } + const { subPath } = incrementalResult; + if (subPath !== undefined) { + path = [...path, ...subPath]; + } + } + setValue(executionResult, path.join('.'), data, { + merge: true, + }); + } + + if (incrementalResult.errors) { + executionResult.errors ||= []; + (executionResult.errors as GraphQLError[]).push( + ...incrementalResult.errors, + ); + } + + if (incrementalResult.extensions) { + setValue(executionResult, 'extensions', incrementalResult.extensions, { + merge: true, + }); + } + + if (incrementalResult.incremental) { + for (const incrementalSubResult of incrementalResult.incremental) { + mergeIncrementalResult(executionResult, incrementalSubResult); + } + } + + if (incrementalResult.completed) { + // Remove tracking and add additional errors + for (const { id, errors } of incrementalResult.completed) { + pathsMap.get(executionResult)?.delete(id); + + if (errors) { + executionResult.errors ||= []; + (executionResult.errors as GraphQLError[]).push(...errors); + } + } + } +} diff --git a/packages/graphiql-react/src/explorer/components/doc-explorer.css b/packages/graphiql-react/src/explorer/components/doc-explorer.css index 87c81282175..9cab5ffb4a0 100644 --- a/packages/graphiql-react/src/explorer/components/doc-explorer.css +++ b/packages/graphiql-react/src/explorer/components/doc-explorer.css @@ -36,7 +36,7 @@ left: 0; } - & [role='combobox'] { + &:not(:focus-within) [role='combobox'] { height: 24px; width: 4ch; } diff --git a/packages/graphiql-react/src/explorer/components/search.css b/packages/graphiql-react/src/explorer/components/search.css index 236f6106588..8d1a4d02b1e 100644 --- a/packages/graphiql-react/src/explorer/components/search.css +++ b/packages/graphiql-react/src/explorer/components/search.css @@ -5,7 +5,7 @@ border: var(--popover-border); border-radius: var(--border-radius-4); box-shadow: var(--popover-box-shadow); - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); & .graphiql-doc-explorer-search-input { background: hsl(var(--color-base)); diff --git a/packages/graphiql-react/src/history/style.css b/packages/graphiql-react/src/history/style.css index c12302d0301..ac536135f29 100644 --- a/packages/graphiql-react/src/history/style.css +++ b/packages/graphiql-react/src/history/style.css @@ -26,7 +26,7 @@ height: 34px; &:hover { - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); background-color: hsla(var(--color-neutral), var(--alpha-background-light)); } @@ -91,7 +91,7 @@ button.graphiql-history-item-action { padding: var(--px-8) var(--px-6); &:hover { - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); } & > svg { diff --git a/packages/graphiql-react/src/icons/argument.svg b/packages/graphiql-react/src/icons/argument.svg index 3a981c90c84..5fd2025608a 100644 --- a/packages/graphiql-react/src/icons/argument.svg +++ b/packages/graphiql-react/src/icons/argument.svg @@ -1,4 +1,13 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/chevron-down.svg b/packages/graphiql-react/src/icons/chevron-down.svg index d02fbfb48c2..1bf38d47c2a 100644 --- a/packages/graphiql-react/src/icons/chevron-down.svg +++ b/packages/graphiql-react/src/icons/chevron-down.svg @@ -1,3 +1,8 @@ - - + + diff --git a/packages/graphiql-react/src/icons/chevron-left.svg b/packages/graphiql-react/src/icons/chevron-left.svg index b740c1d21df..0f5a0f2af42 100644 --- a/packages/graphiql-react/src/icons/chevron-left.svg +++ b/packages/graphiql-react/src/icons/chevron-left.svg @@ -1,3 +1,12 @@ - - + + diff --git a/packages/graphiql-react/src/icons/chevron-up.svg b/packages/graphiql-react/src/icons/chevron-up.svg index 617bf886d24..ff4486e1de7 100644 --- a/packages/graphiql-react/src/icons/chevron-up.svg +++ b/packages/graphiql-react/src/icons/chevron-up.svg @@ -1,3 +1,8 @@ - - + + diff --git a/packages/graphiql-react/src/icons/close.svg b/packages/graphiql-react/src/icons/close.svg index 229315632b3..2d835365a1e 100644 --- a/packages/graphiql-react/src/icons/close.svg +++ b/packages/graphiql-react/src/icons/close.svg @@ -1,4 +1,10 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/copy.svg b/packages/graphiql-react/src/icons/copy.svg index c18b6e78a00..578e644ee4d 100644 --- a/packages/graphiql-react/src/icons/copy.svg +++ b/packages/graphiql-react/src/icons/copy.svg @@ -1,4 +1,21 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/deprecated-argument.svg b/packages/graphiql-react/src/icons/deprecated-argument.svg index 5da9ce09f52..87752fe5ffe 100644 --- a/packages/graphiql-react/src/icons/deprecated-argument.svg +++ b/packages/graphiql-react/src/icons/deprecated-argument.svg @@ -1,5 +1,14 @@ - - - - + + + + diff --git a/packages/graphiql-react/src/icons/deprecated-enum-value.svg b/packages/graphiql-react/src/icons/deprecated-enum-value.svg index 8e44d419817..4b1a81034fe 100644 --- a/packages/graphiql-react/src/icons/deprecated-enum-value.svg +++ b/packages/graphiql-react/src/icons/deprecated-enum-value.svg @@ -1,5 +1,15 @@ - - - - + + + + diff --git a/packages/graphiql-react/src/icons/deprecated-field.svg b/packages/graphiql-react/src/icons/deprecated-field.svg index fd07672d975..8fd99c896a0 100644 --- a/packages/graphiql-react/src/icons/deprecated-field.svg +++ b/packages/graphiql-react/src/icons/deprecated-field.svg @@ -1,5 +1,18 @@ - - - - + + + + diff --git a/packages/graphiql-react/src/icons/directive.svg b/packages/graphiql-react/src/icons/directive.svg index 5cdd18314ff..14b8ed1ff38 100644 --- a/packages/graphiql-react/src/icons/directive.svg +++ b/packages/graphiql-react/src/icons/directive.svg @@ -1,4 +1,17 @@ - - + + diff --git a/packages/graphiql-react/src/icons/docs-filled.svg b/packages/graphiql-react/src/icons/docs-filled.svg index 15e197e760d..56938f94158 100644 --- a/packages/graphiql-react/src/icons/docs-filled.svg +++ b/packages/graphiql-react/src/icons/docs-filled.svg @@ -1,5 +1,23 @@ - - - - + + + + diff --git a/packages/graphiql-react/src/icons/docs.svg b/packages/graphiql-react/src/icons/docs.svg index 4c2bf68a40c..55869ef615b 100644 --- a/packages/graphiql-react/src/icons/docs.svg +++ b/packages/graphiql-react/src/icons/docs.svg @@ -1,4 +1,20 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/enum-value.svg b/packages/graphiql-react/src/icons/enum-value.svg index d677ef519f0..5132bb75308 100644 --- a/packages/graphiql-react/src/icons/enum-value.svg +++ b/packages/graphiql-react/src/icons/enum-value.svg @@ -1,4 +1,14 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/field.svg b/packages/graphiql-react/src/icons/field.svg index 58bfbfa987f..ab8c7f6390f 100644 --- a/packages/graphiql-react/src/icons/field.svg +++ b/packages/graphiql-react/src/icons/field.svg @@ -1,4 +1,17 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/history.svg b/packages/graphiql-react/src/icons/history.svg index 3a632094812..ef69e5c9b70 100644 --- a/packages/graphiql-react/src/icons/history.svg +++ b/packages/graphiql-react/src/icons/history.svg @@ -1,5 +1,24 @@ - - - - + + + + diff --git a/packages/graphiql-react/src/icons/implements.svg b/packages/graphiql-react/src/icons/implements.svg index 31b7ba1c310..43b3374052a 100644 --- a/packages/graphiql-react/src/icons/implements.svg +++ b/packages/graphiql-react/src/icons/implements.svg @@ -1,4 +1,18 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/index.ts b/packages/graphiql-react/src/icons/index.ts deleted file mode 100644 index bc4c4ad5758..00000000000 --- a/packages/graphiql-react/src/icons/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ComponentProps, FC } from 'react'; - -import _ArgumentIcon from './argument.svg'; -import _ChevronDownIcon from './chevron-down.svg'; -import _ChevronLeftIcon from './chevron-left.svg'; -import _ChevronUpIcon from './chevron-up.svg'; -import _CloseIcon from './close.svg'; -import _CopyIcon from './copy.svg'; -import _DeprecatedArgumentIcon from './deprecated-argument.svg'; -import _DeprecatedEnumValueIcon from './deprecated-enum-value.svg'; -import _DeprecatedFieldIcon from './deprecated-field.svg'; -import _DirectiveIcon from './directive.svg'; -import _DocsFilledIcon from './docs-filled.svg'; -import _DocsIcon from './docs.svg'; -import _EnumValueIcon from './enum-value.svg'; -import _FieldIcon from './field.svg'; -import _HistoryIcon from './history.svg'; -import _ImplementsIcon from './implements.svg'; -import _KeyboardShortcutIcon from './keyboard-shortcut.svg'; -import _MagnifyingGlassIcon from './magnifying-glass.svg'; -import _MergeIcon from './merge.svg'; -import _PenIcon from './pen.svg'; -import _PlayIcon from './play.svg'; -import _PlusIcon from './plus.svg'; -import _PrettifyIcon from './prettify.svg'; -import _ReloadIcon from './reload.svg'; -import _RootTypeIcon from './root-type.svg'; -import _SettingsIcon from './settings.svg'; -import _StarFilledIcon from './star-filled.svg'; -import _StarIcon from './star.svg'; -import _StopIcon from './stop.svg'; -import _TrashIcon from './trash.svg'; -import _TypeIcon from './type.svg'; - -export const ArgumentIcon = generateIcon(_ArgumentIcon); -export const ChevronDownIcon = generateIcon(_ChevronDownIcon); -export const ChevronLeftIcon = generateIcon(_ChevronLeftIcon); -export const ChevronUpIcon = generateIcon(_ChevronUpIcon); -export const CloseIcon = generateIcon(_CloseIcon); -export const CopyIcon = generateIcon(_CopyIcon); -export const DeprecatedArgumentIcon = generateIcon(_DeprecatedArgumentIcon); -export const DeprecatedEnumValueIcon = generateIcon(_DeprecatedEnumValueIcon); -export const DeprecatedFieldIcon = generateIcon(_DeprecatedFieldIcon); -export const DirectiveIcon = generateIcon(_DirectiveIcon); -export const DocsFilledIcon = generateIcon(_DocsFilledIcon, 'filled docs icon'); -export const DocsIcon = generateIcon(_DocsIcon); -export const EnumValueIcon = generateIcon(_EnumValueIcon); -export const FieldIcon = generateIcon(_FieldIcon); -export const HistoryIcon = generateIcon(_HistoryIcon); -export const ImplementsIcon = generateIcon(_ImplementsIcon); -export const KeyboardShortcutIcon = generateIcon(_KeyboardShortcutIcon); -export const MagnifyingGlassIcon = generateIcon(_MagnifyingGlassIcon); -export const MergeIcon = generateIcon(_MergeIcon); -export const PenIcon = generateIcon(_PenIcon); -export const PlayIcon = generateIcon(_PlayIcon); -export const PlusIcon = generateIcon(_PlusIcon); -export const PrettifyIcon = generateIcon(_PrettifyIcon); -export const ReloadIcon = generateIcon(_ReloadIcon); -export const RootTypeIcon = generateIcon(_RootTypeIcon); -export const SettingsIcon = generateIcon(_SettingsIcon); -export const StarFilledIcon = generateIcon(_StarFilledIcon, 'filled star icon'); -export const StarIcon = generateIcon(_StarIcon); -export const StopIcon = generateIcon(_StopIcon); -export const TrashIcon = generateIcon(_TrashIcon, 'trash icon'); -export const TypeIcon = generateIcon(_TypeIcon); - -function generateIcon( - RawComponent: any, - title = RawComponent.name - // Icon component name starts with `Svg${CamelCaseFilename without .svg}` - .replace('Svg', '') - // Insert a space before all caps - .replaceAll(/([A-Z])/g, ' $1') - .trimStart() - .toLowerCase() + ' icon', -): FC> { - RawComponent.defaultProps = { title }; - return RawComponent; -} diff --git a/packages/graphiql-react/src/icons/index.tsx b/packages/graphiql-react/src/icons/index.tsx new file mode 100644 index 00000000000..097ae61541e --- /dev/null +++ b/packages/graphiql-react/src/icons/index.tsx @@ -0,0 +1,85 @@ +import { ComponentProps, FC, SVGProps } from 'react'; + +import _ArgumentIcon from './argument.svg?react'; +import _ChevronDownIcon from './chevron-down.svg?react'; +import _ChevronLeftIcon from './chevron-left.svg?react'; +import _ChevronUpIcon from './chevron-up.svg?react'; +import _CloseIcon from './close.svg?react'; +import _CopyIcon from './copy.svg?react'; +import _DeprecatedArgumentIcon from './deprecated-argument.svg?react'; +import _DeprecatedEnumValueIcon from './deprecated-enum-value.svg?react'; +import _DeprecatedFieldIcon from './deprecated-field.svg?react'; +import _DirectiveIcon from './directive.svg?react'; +import _DocsFilledIcon from './docs-filled.svg?react'; +import _DocsIcon from './docs.svg?react'; +import _EnumValueIcon from './enum-value.svg?react'; +import _FieldIcon from './field.svg?react'; +import _HistoryIcon from './history.svg?react'; +import _ImplementsIcon from './implements.svg?react'; +import _KeyboardShortcutIcon from './keyboard-shortcut.svg?react'; +import _MagnifyingGlassIcon from './magnifying-glass.svg?react'; +import _MergeIcon from './merge.svg?react'; +import _PenIcon from './pen.svg?react'; +import _PlayIcon from './play.svg?react'; +import _PlusIcon from './plus.svg?react'; +import _PrettifyIcon from './prettify.svg?react'; +import _ReloadIcon from './reload.svg?react'; +import _RootTypeIcon from './root-type.svg?react'; +import _SettingsIcon from './settings.svg?react'; +import _StarFilledIcon from './star-filled.svg?react'; +import _StarIcon from './star.svg?react'; +import _StopIcon from './stop.svg?react'; +import _TrashIcon from './trash.svg?react'; +import _TypeIcon from './type.svg?react'; + +export const ArgumentIcon = generateIcon(_ArgumentIcon); +export const ChevronDownIcon = generateIcon(_ChevronDownIcon); +export const ChevronLeftIcon = generateIcon(_ChevronLeftIcon); +export const ChevronUpIcon = generateIcon(_ChevronUpIcon); +export const CloseIcon = generateIcon(_CloseIcon); +export const CopyIcon = generateIcon(_CopyIcon); +export const DeprecatedArgumentIcon = generateIcon(_DeprecatedArgumentIcon); +export const DeprecatedEnumValueIcon = generateIcon(_DeprecatedEnumValueIcon); +export const DeprecatedFieldIcon = generateIcon(_DeprecatedFieldIcon); +export const DirectiveIcon = generateIcon(_DirectiveIcon); +export const DocsFilledIcon = generateIcon(_DocsFilledIcon); +export const DocsIcon = generateIcon(_DocsIcon); +export const EnumValueIcon = generateIcon(_EnumValueIcon); +export const FieldIcon = generateIcon(_FieldIcon); +export const HistoryIcon = generateIcon(_HistoryIcon); +export const ImplementsIcon = generateIcon(_ImplementsIcon); +export const KeyboardShortcutIcon = generateIcon(_KeyboardShortcutIcon); +export const MagnifyingGlassIcon = generateIcon(_MagnifyingGlassIcon); +export const MergeIcon = generateIcon(_MergeIcon); +export const PenIcon = generateIcon(_PenIcon); +export const PlayIcon = generateIcon(_PlayIcon); +export const PlusIcon = generateIcon(_PlusIcon); +export const PrettifyIcon = generateIcon(_PrettifyIcon); +export const ReloadIcon = generateIcon(_ReloadIcon); +export const RootTypeIcon = generateIcon(_RootTypeIcon); +export const SettingsIcon = generateIcon(_SettingsIcon); +export const StarFilledIcon = generateIcon(_StarFilledIcon); +export const StarIcon = generateIcon(_StarIcon); +export const StopIcon = generateIcon(_StopIcon); +export const TrashIcon = generateIcon(_TrashIcon); +export const TypeIcon = generateIcon(_TypeIcon); + +function generateIcon( + RawComponent: FC & { title?: string }>, +): FC> { + const title = + RawComponent.name + // Icon component name starts with `Svg${CamelCaseFilename without .svg}` + .replace('Svg', '') + // Insert a space before all caps + .replaceAll(/([A-Z])/g, ' $1') + .trimStart() + .toLowerCase() + ' icon'; + + function IconComponent(props: ComponentProps<'svg'>) { + return ; + } + + IconComponent.displayName = RawComponent.name; + return IconComponent; +} diff --git a/packages/graphiql-react/src/icons/keyboard-shortcut.svg b/packages/graphiql-react/src/icons/keyboard-shortcut.svg index c578c7715ed..03beab5acc6 100644 --- a/packages/graphiql-react/src/icons/keyboard-shortcut.svg +++ b/packages/graphiql-react/src/icons/keyboard-shortcut.svg @@ -1,7 +1,42 @@ - - - - - - + + + + + + diff --git a/packages/graphiql-react/src/icons/magnifying-glass.svg b/packages/graphiql-react/src/icons/magnifying-glass.svg index b2593871633..67ee280f9f0 100644 --- a/packages/graphiql-react/src/icons/magnifying-glass.svg +++ b/packages/graphiql-react/src/icons/magnifying-glass.svg @@ -1,4 +1,16 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/merge.svg b/packages/graphiql-react/src/icons/merge.svg index c4db221b4d8..e007c1df3c5 100644 --- a/packages/graphiql-react/src/icons/merge.svg +++ b/packages/graphiql-react/src/icons/merge.svg @@ -1,6 +1,19 @@ - - - - - + + + + + diff --git a/packages/graphiql-react/src/icons/pen.svg b/packages/graphiql-react/src/icons/pen.svg index 365a3b2431a..9f252d9ba54 100644 --- a/packages/graphiql-react/src/icons/pen.svg +++ b/packages/graphiql-react/src/icons/pen.svg @@ -1,5 +1,25 @@ - - - - + + + + diff --git a/packages/graphiql-react/src/icons/play.svg b/packages/graphiql-react/src/icons/play.svg index 9f194110940..b5bd34659f0 100644 --- a/packages/graphiql-react/src/icons/play.svg +++ b/packages/graphiql-react/src/icons/play.svg @@ -1,3 +1,11 @@ - - + + diff --git a/packages/graphiql-react/src/icons/plus.svg b/packages/graphiql-react/src/icons/plus.svg index 5d02b3c4a5f..886a1b4e572 100644 --- a/packages/graphiql-react/src/icons/plus.svg +++ b/packages/graphiql-react/src/icons/plus.svg @@ -1,3 +1,12 @@ - - + + diff --git a/packages/graphiql-react/src/icons/prettify.svg b/packages/graphiql-react/src/icons/prettify.svg index 490de60576e..516ffa5d95b 100644 --- a/packages/graphiql-react/src/icons/prettify.svg +++ b/packages/graphiql-react/src/icons/prettify.svg @@ -1,7 +1,35 @@ - - - - - - + + + + + + diff --git a/packages/graphiql-react/src/icons/reload.svg b/packages/graphiql-react/src/icons/reload.svg index 853c18128fa..553b4e16f54 100644 --- a/packages/graphiql-react/src/icons/reload.svg +++ b/packages/graphiql-react/src/icons/reload.svg @@ -1,6 +1,29 @@ - - - - - + + + + + diff --git a/packages/graphiql-react/src/icons/root-type.svg b/packages/graphiql-react/src/icons/root-type.svg index 29ffd5a325a..30b0e69f5f8 100644 --- a/packages/graphiql-react/src/icons/root-type.svg +++ b/packages/graphiql-react/src/icons/root-type.svg @@ -1,4 +1,21 @@ - - - + + + diff --git a/packages/graphiql-react/src/icons/settings.svg b/packages/graphiql-react/src/icons/settings.svg index f7cf68be0a1..9c0b128fdb2 100644 --- a/packages/graphiql-react/src/icons/settings.svg +++ b/packages/graphiql-react/src/icons/settings.svg @@ -1,3 +1,13 @@ - - + + diff --git a/packages/graphiql-react/src/icons/star-filled.svg b/packages/graphiql-react/src/icons/star-filled.svg index 3c71764a882..27c2cd13ab4 100644 --- a/packages/graphiql-react/src/icons/star-filled.svg +++ b/packages/graphiql-react/src/icons/star-filled.svg @@ -1,3 +1,12 @@ - - + + diff --git a/packages/graphiql-react/src/icons/star.svg b/packages/graphiql-react/src/icons/star.svg index 8399e72b0d7..b9fdcffa594 100644 --- a/packages/graphiql-react/src/icons/star.svg +++ b/packages/graphiql-react/src/icons/star.svg @@ -1,3 +1,12 @@ - - + + diff --git a/packages/graphiql-react/src/icons/stop.svg b/packages/graphiql-react/src/icons/stop.svg index 02d9a7b321a..defca5fb2eb 100644 --- a/packages/graphiql-react/src/icons/stop.svg +++ b/packages/graphiql-react/src/icons/stop.svg @@ -1,3 +1,8 @@ - - + + diff --git a/packages/graphiql-react/src/icons/trash.svg b/packages/graphiql-react/src/icons/trash.svg index 91917ac8f29..9cd42408f9c 100644 --- a/packages/graphiql-react/src/icons/trash.svg +++ b/packages/graphiql-react/src/icons/trash.svg @@ -1,5 +1,17 @@ -\\" does not.' - : 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \\"\\" does not.', + version.startsWith('15') + ? 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \\"\\" does not.' + : 'Names must only contain [_a-zA-Z0-9] but \\"\\" does not.', ); }); }); diff --git a/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts b/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts index 0ab5710b5ea..1ea0c0ed822 100644 --- a/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts +++ b/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts @@ -1,10 +1,9 @@ import { version } from 'graphql'; -let describeOrSkip = describe.skip; +let describeOrSkip: Mocha.SuiteFunction | Mocha.PendingSuiteFunction = describe; -// TODO: disable when defer/stream is merged to graphql -if (version.includes('stream')) { - describeOrSkip = describe; +if (parseInt(version, 10) < 17) { + describeOrSkip = describe.skip; } describeOrSkip('IncrementalDelivery support via fetcher', () => { @@ -52,7 +51,6 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => { }, ], }, - hasNext: false, }; it('Expects slower streams to resolve in several increments, and the payloads to patch properly', () => { @@ -99,7 +97,6 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => { 'Oops, this took 1 seconds longer than I thought it would!', }, }, - hasNext: false, }); }); @@ -164,7 +161,6 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => { age: 1000, }, }, - hasNext: false, }); }); }); diff --git a/packages/graphiql/cypress/e2e/init.cy.ts b/packages/graphiql/cypress/e2e/init.cy.ts index 960faecd627..15f2b84e39d 100644 --- a/packages/graphiql/cypress/e2e/init.cy.ts +++ b/packages/graphiql/cypress/e2e/init.cy.ts @@ -49,7 +49,8 @@ describe('GraphiQL On Initialization', () => { cy.assertQueryResult(mockSuccess); }); it('Shows the expected error when the schema is invalid', () => { - cy.visit('/?bad=true'); + cy.intercept('/graphql', { fixture: 'bad-schema.json' }); + cy.visit('/'); cy.get('section.result-window').should(element => { expect(element.get(0).innerText).to.contain('Names must'); }); diff --git a/packages/graphiql/cypress/e2e/theme.cy.ts b/packages/graphiql/cypress/e2e/theme.cy.ts new file mode 100644 index 00000000000..456b99511de --- /dev/null +++ b/packages/graphiql/cypress/e2e/theme.cy.ts @@ -0,0 +1,17 @@ +describe('Theme', () => { + it('Switches to light theme when `forcedTheme` is light', () => { + cy.visit('/?query={test}&forcedTheme=light'); + cy.get('body').should('have.class', 'graphiql-light'); + }); + + it('Switches to dark theme when `forcedTheme` is dark', () => { + cy.visit('/?query={test}&forcedTheme=dark'); + cy.get('body').should('have.class', 'graphiql-dark'); + }); + + it('Defaults to light theme when `forcedTheme` value is invalid', () => { + cy.visit('/?query={test}&forcedTheme=invalid'); + cy.get('[data-value=settings]').click(); + cy.get('.graphiql-dialog-section-title').eq(1).should('have.text', 'Theme'); // Check for the presence of the theme dialog + }); +}); diff --git a/packages/graphiql/cypress/fixtures/bad-schema.json b/packages/graphiql/cypress/fixtures/bad-schema.json new file mode 100644 index 00000000000..b05417a54bf --- /dev/null +++ b/packages/graphiql/cypress/fixtures/bad-schema.json @@ -0,0 +1,99 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "user", + "description": null, + "args": [ + { + "name": "id", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [] + } + } +} diff --git a/packages/graphiql/jest.config.js b/packages/graphiql/jest.config.js index 22b3f1af305..4b9d01d0279 100644 --- a/packages/graphiql/jest.config.js +++ b/packages/graphiql/jest.config.js @@ -3,7 +3,7 @@ const base = require('../../jest.config.base')(__dirname); module.exports = { ...base, moduleNameMapper: { - '\\.svg$': `${__dirname}/__mocks__/svg`, + '\\.svg\\?react$': `${__dirname}/__mocks__/svg`, ...base.moduleNameMapper, }, }; diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json index 2a06646b43b..3660963383d 100644 --- a/packages/graphiql/package.json +++ b/packages/graphiql/package.json @@ -1,6 +1,6 @@ { "name": "graphiql", - "version": "3.1.1", + "version": "3.5.0", "description": "An graphical interactive in-browser GraphQL IDE.", "contributors": [ "Hyohyeon Jeong ", @@ -47,43 +47,36 @@ "webpack": "webpack-cli --config resources/webpack.config.js" }, "dependencies": { - "@graphiql/react": "^0.20.3", - "@graphiql/toolkit": "^0.9.1", - "graphql-language-service": "^5.2.0", - "markdown-it": "^12.2.0" + "@graphiql/react": "^0.24.0" }, "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2", "react": "^16.8.0 || ^17 || ^18", "react-dom": "^16.8.0 || ^17 || ^18" }, "devDependencies": { - "@cypress/webpack-preprocessor": "^5.5.0", + "@graphiql/toolkit": "^0.10.0", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.0.0", - "@types/codemirror": "^0.0.90", - "@types/markdown-it": "^12.2.3", - "@types/node": "^16.18.4", "@types/testing-library__jest-dom": "5.14.5", "babel-loader": "^9.1.2", "babel-plugin-macros": "^3.1.0", "cross-env": "^7.0.2", "css-loader": "^6.7.3", "cssnano": "^5.1.15", - "cypress": "^12.6.0", - "express": "^4.18.2", + "cypress": "^13.13.2", + "express": "^4.19.2", "fork-ts-checker-webpack-plugin": "7.3.0", - "graphql": "^16.8.1", - "graphql-http": "^1.19.0", + "graphql": "^17.0.0-alpha.7", + "graphql-helix": "^1.13.0", "graphql-subscriptions": "^2.0.0", "html-webpack-plugin": "^5.5.0", "identity-obj-proxy": "^3.0.0", "mini-css-extract-plugin": "^2.7.2", - "postcss": "8.4.21", + "postcss": "8.4.31", "postcss-import": "15.1.0", "postcss-loader": "7.0.2", "postcss-preset-env": "^8.0.1", - "prop-types": "15.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hot-loader": "^4.12.20", @@ -99,6 +92,6 @@ "webpack-bundle-analyzer": "^3.6.1", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1", - "ws": "8.8.1" + "ws": "8.17.1" } } diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index f78403dde30..017eea99397 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -63,16 +63,6 @@ function getSchemaUrl() { const isDev = window.location.hostname.match(/localhost$/); if (isDev) { - // This supports an e2e test which ensures that invalid schemas do not load. - if (parameters.bad === 'true') { - return '/bad/graphql'; - } - if (parameters['http-error'] === 'true') { - return '/http-error/graphql'; - } - if (parameters['graphql-error'] === 'true') { - return '/graphql-error/graphql'; - } return '/graphql'; } return '/.netlify/functions/graphql'; @@ -102,5 +92,6 @@ root.render( shouldPersistHeaders: true, inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, onTabChange, + forcedTheme: parameters.forcedTheme, }), ); diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index fb2a44522ff..cbc85c95775 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -14,6 +14,8 @@ import React, { ReactElement, useCallback, useState, + useEffect, + useMemo, } from 'react'; import { @@ -168,6 +170,7 @@ export function GraphiQL({ @@ -216,8 +219,20 @@ export type GraphiQLInterfaceProps = WriteableEditorProps & */ showPersistHeadersSettings?: boolean; disableTabs?: boolean; + /** + * forcedTheme allows enforcement of a specific theme for GraphiQL. + * This is useful when you want to make sure that GraphiQL is always + * rendered with a specific theme + */ + forcedTheme?: (typeof THEMES)[number]; + /** + * Additional class names which will be appended to the container element. + */ + className?: string; }; +const THEMES = ['light', 'dark', 'system'] as const; + export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true; const editorContext = useEditorContext({ nonNull: true }); @@ -225,6 +240,13 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const schemaContext = useSchemaContext({ nonNull: true }); const storageContext = useStorageContext(); const pluginContext = usePluginContext(); + const forcedTheme = useMemo( + () => + props.forcedTheme && THEMES.includes(props.forcedTheme) + ? props.forcedTheme + : undefined, + [props.forcedTheme], + ); const copy = useCopyQuery({ onCopyQuery: props.onCopyQuery }); const merge = useMergeQuery(); @@ -232,6 +254,14 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const { theme, setTheme } = useTheme(); + useEffect(() => { + if (forcedTheme === 'system') { + setTheme(null); + } else if (forcedTheme === 'light' || forcedTheme === 'dark') { + setTheme(forcedTheme); + } + }, [forcedTheme, setTheme]); + const PluginContent = pluginContext?.visiblePlugin?.content; const pluginResize = useDragResize({ @@ -317,7 +347,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { - {props.toolbar?.additionalContent && props.toolbar.additionalContent} + {props.toolbar?.additionalContent} {props.toolbar?.additionalComponent && ( )} @@ -439,9 +469,14 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { ); + const className = props.className ? ` ${props.className}` : ''; + return ( -
+
{pluginContext?.plugins.map((plugin, index) => { @@ -520,7 +555,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { )}
- {props.disableTabs ? null : ( + {!props.disableTabs && (
) : null} -
-
-
Theme
-
- Adjust how the interface looks like. + {!forcedTheme && ( +
+
+
Theme
+
+ Adjust how the interface appears. +
+ + + + +
- - - - - -
+ )} {storageContext ? (
diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 8eacd3b6719..e278a1310fe 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -29,7 +29,7 @@ const simpleIntrospection = { }; beforeEach(() => { - window.localStorage.clear(); + localStorage.clear(); }); describe('GraphiQL', () => { diff --git a/packages/graphiql/src/style.css b/packages/graphiql/src/style.css index aa3120e4a59..ed695eac028 100644 --- a/packages/graphiql/src/style.css +++ b/packages/graphiql/src/style.css @@ -33,7 +33,7 @@ } .graphiql-container .graphiql-sidebar button.active { - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); } .graphiql-container .graphiql-sidebar button:not(:first-child) { @@ -169,7 +169,7 @@ button.graphiql-tab-add > svg { } .graphiql-container .graphiql-editor-tools button.active { - color: hsla(var(--color-neutral), 1); + color: hsl(var(--color-neutral)); } /* The tab buttons to switch between editor tools */ diff --git a/packages/graphiql/test/afterDevServer.js b/packages/graphiql/test/afterDevServer.js index d47ef13f274..6d54fa923fa 100644 --- a/packages/graphiql/test/afterDevServer.js +++ b/packages/graphiql/test/afterDevServer.js @@ -1,4 +1,4 @@ -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line import-x/no-extraneous-dependencies const { useServer } = require('graphql-ws/lib/use/ws'); const { Server: WebSocketServer } = require('ws'); const schema = require('./schema'); @@ -10,4 +10,5 @@ module.exports = function afterDevServer(_app, _server, _compiler) { }); // eslint-disable-next-line react-hooks/rules-of-hooks useServer({ schema }, wsServer); + return wsServer; }; diff --git a/packages/graphiql/test/bad-schema.js b/packages/graphiql/test/bad-schema.js deleted file mode 100644 index 2ec51e0625c..00000000000 --- a/packages/graphiql/test/bad-schema.js +++ /dev/null @@ -1,97 +0,0 @@ -module.exports.schema = { - __schema: { - queryType: { - name: 'Query', - }, - mutationType: null, - subscriptionType: null, - types: [ - { - kind: 'OBJECT', - name: 'Query', - description: null, - fields: [ - { - name: 'user', - description: null, - args: [ - { - name: 'id', - description: null, - type: { - kind: 'NON_NULL', - name: null, - ofType: { - kind: 'SCALAR', - name: 'ID', - ofType: null, - }, - }, - defaultValue: null, - }, - ], - type: { - kind: 'OBJECT', - name: '', - ofType: null, - }, - isDeprecated: false, - deprecationReason: null, - }, - ], - inputFields: null, - interfaces: [], - enumValues: null, - possibleTypes: null, - }, - { - kind: 'SCALAR', - name: 'ID', - description: '', - fields: null, - inputFields: null, - interfaces: null, - enumValues: null, - possibleTypes: null, - }, - { - kind: 'OBJECT', - name: '', - description: null, - fields: [ - { - name: 'name', - description: null, - args: [], - type: { - kind: 'NON_NULL', - name: null, - ofType: { - kind: 'SCALAR', - name: 'String', - ofType: null, - }, - }, - isDeprecated: false, - deprecationReason: null, - }, - ], - inputFields: null, - interfaces: [], - enumValues: null, - possibleTypes: null, - }, - { - kind: 'SCALAR', - name: 'String', - description: '', - fields: null, - inputFields: null, - interfaces: null, - enumValues: null, - possibleTypes: null, - }, - ], - directives: [], - }, -}; diff --git a/packages/graphiql/test/beforeDevServer.js b/packages/graphiql/test/beforeDevServer.js deleted file mode 100644 index d386ae47922..00000000000 --- a/packages/graphiql/test/beforeDevServer.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2021 GraphQL Contributors. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const express = require('express'); -const path = require('node:path'); -const { createHandler } = require('graphql-http/lib/use/express'); -const schema = require('./schema'); -const { schema: badSchema } = require('./bad-schema'); - -module.exports = function beforeDevServer(app, _server, _compiler) { - // GraphQL Server - app.post('/graphql', createHandler({ schema })); - app.get('/graphql', createHandler({ schema })); - - app.post('/bad/graphql', (_req, res, next) => { - res.json({ data: badSchema }); - next(); - }); - - app.use('/images', express.static(path.join(__dirname, 'images'))); - - app.use( - '/resources/renderExample.js', - express.static(path.join(__dirname, '../resources/renderExample.js')), - ); -}; diff --git a/packages/graphiql/test/e2e-server.js b/packages/graphiql/test/e2e-server.js index a714e5be590..970b8144162 100644 --- a/packages/graphiql/test/e2e-server.js +++ b/packages/graphiql/test/e2e-server.js @@ -6,44 +6,55 @@ */ /* eslint-disable no-console */ +const { createServer } = require('node:http'); const express = require('express'); const path = require('node:path'); -const { createHandler } = require('graphql-http/lib/use/express'); -const { GraphQLError } = require('graphql'); +const { + getGraphQLParameters, + processRequest, + sendResult, +} = require('graphql-helix'); // update when `graphql-http` is upgraded to support multipart requests for incremental delivery https://github.com/graphql/graphiql/pull/3682#discussion_r1715545279 +const WebSocketsServer = require('./afterDevServer'); const schema = require('./schema'); +const { customExecute } = require('./execute'); + const app = express(); -const { schema: badSchema } = require('./bad-schema'); -const WebSocketsServer = require('./afterDevServer'); -// Server -app.post('/graphql', createHandler({ schema })); +async function handler(req, res) { + const request = { + body: req.body, + headers: req.headers, + method: req.method, + query: req.query, + }; + + const { operationName, query, variables } = getGraphQLParameters(request); -app.get( - '/graphql', - createHandler({ + const result = await processRequest({ + operationName, + query, + variables, + request, schema, - }), -); + execute: customExecute, + }); -app.post('/bad/graphql', (_req, res, next) => { - res.json({ data: badSchema }); - next(); -}); + sendResult(result, res); +} -app.post('/http-error/graphql', (_req, res, next) => { - res.status(502).send('Bad Gateway'); - next(); -}); +// Server +app.use(express.json()); -app.post('/graphql-error/graphql', (_req, res, next) => { - res.json({ errors: [new GraphQLError('Something unexpected happened...')] }); - next(); -}); +app.post('/graphql', handler); +app.get('/graphql', handler); app.use(express.static(path.resolve(__dirname, '../'))); app.use('index.html', express.static(path.resolve(__dirname, '../dev.html'))); -app.listen(process.env.PORT || 0, function () { +// messy but it allows close +const server = createServer(app); + +server.listen(process.env.PORT || 3100, function () { const { port } = this.address(); console.log(`Started on http://localhost:${port}/`); diff --git a/packages/graphiql/test/execute.js b/packages/graphiql/test/execute.js new file mode 100644 index 00000000000..1ca1ed1f7ca --- /dev/null +++ b/packages/graphiql/test/execute.js @@ -0,0 +1,28 @@ +const { + execute, + experimentalExecuteIncrementally, + version, +} = require('graphql'); + +const customExecute = + parseInt(version, 10) > 16 + ? async (...args) => { + const result = await experimentalExecuteIncrementally(...args); + + if (!('subsequentResults' in result)) { + return result; + } + + const { initialResult, subsequentResults } = result; + if (typeof subsequentResults[Symbol.asyncIterator] !== 'function') { + return result; + } + + return (async function* () { + yield initialResult; + yield* subsequentResults; + })(); + } + : execute; + +module.exports = { customExecute }; diff --git a/packages/graphiql/test/images/logo.svg b/packages/graphiql/test/images/logo.svg index 337843aca18..43e513e06ca 100644 --- a/packages/graphiql/test/images/logo.svg +++ b/packages/graphiql/test/images/logo.svg @@ -1 +1,27 @@ - + + + + + + + + + diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js index fcd648096f1..d6c048e5514 100644 --- a/packages/graphiql/test/schema.js +++ b/packages/graphiql/test/schema.js @@ -6,6 +6,8 @@ * LICENSE file in the root directory of this source tree. */ +const graphql = require('graphql'); + const { GraphQLSchema, GraphQLObjectType, @@ -19,7 +21,16 @@ const { GraphQLString, GraphQLID, GraphQLList, -} = require('graphql'); + GraphQLDeferDirective, + GraphQLStreamDirective, + specifiedDirectives, + version, +} = graphql; + +const directives = + parseInt(version, 10) > 16 + ? [...specifiedDirectives, GraphQLDeferDirective, GraphQLStreamDirective] + : specifiedDirectives; // Test Schema const TestEnum = new GraphQLEnumType({ @@ -386,6 +397,7 @@ const myTestSchema = new GraphQLSchema({ mutation: TestMutationType, subscription: TestSubscriptionType, description: 'This is a test schema for GraphiQL', + directives, }); module.exports = myTestSchema; diff --git a/packages/graphql-language-service-cli/CHANGELOG.md b/packages/graphql-language-service-cli/CHANGELOG.md index 7434288d1e4..0a2e4bf41b5 100644 --- a/packages/graphql-language-service-cli/CHANGELOG.md +++ b/packages/graphql-language-service-cli/CHANGELOG.md @@ -1,5 +1,179 @@ # graphql-language-service-cli +## 3.5.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - graphql-language-service-server@2.14.0 + - graphql-language-service@5.3.0 + +## 3.4.2 + +### Patch Changes + +- [#3647](https://github.com/graphql/graphiql/pull/3647) [`ba5720b`](https://github.com/graphql/graphiql/commit/ba5720b430ed1c888ff64c67aa4b9a36083b9ed0) Thanks [@acao](https://github.com/acao)! - several LSP fixes and improvements: + + **Bugfixes** + + debounce schema change events to fix codegen bugs to fix #3622 + + on mass file changes, network schema is overfetching because the schema cache is now invalidated on every watched schema file change + + to address this, we debounce the new `onSchemaChange` event by 400ms + + note that `schemaCacheTTL` can only be set in extension settings or graphql config at the top level - it will be ignored if configured per-project in the graphql config + + **Code Improvements** + + - Fixes flaky tests, and `schemaCacheTTL` setting not being passed to the cache + - Adds a test to validate network schema changes are reflected in the cache + +- Updated dependencies [[`ba5720b`](https://github.com/graphql/graphiql/commit/ba5720b430ed1c888ff64c67aa4b9a36083b9ed0), [`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d), [`e2c04c7`](https://github.com/graphql/graphiql/commit/e2c04c7c2dc5109ff0446d9a6a010ffdffed1e44)]: + - graphql-language-service-server@2.13.2 + - graphql-language-service@5.2.2 + +## 3.4.1 + +### Patch Changes + +- [#3628](https://github.com/graphql/graphiql/pull/3628) [`7fad662f`](https://github.com/graphql/graphiql/commit/7fad662f77eae9f842bb55cb93cb98df33bbc1ed) Thanks [@acao](https://github.com/acao)! - fix the lsp stream interface for stdin/out (neovim, etc) + +- Updated dependencies [[`7fad662f`](https://github.com/graphql/graphiql/commit/7fad662f77eae9f842bb55cb93cb98df33bbc1ed)]: + - graphql-language-service-server@2.13.1 + +## 3.4.0 + +### Minor Changes + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Fix many schema and fragment lifecycle issues, not all of them, but many related to cacheing. Note: this makes `cacheSchemaForLookup` enabled by default again for schema first contexts. + + This fixes multiple cacheing bugs, upon addomg some in-depth integration test coverage for the LSP server. It also solves several bugs regarding loading config types, and properly restarts the server and invalidates schema when there are config changes. + + ### Bugfix Summary + + - configurable polling updates for network and other code first schema configuration, set to a 30s interval by default. powered by `schemaCacheTTL` which can be configured in the IDE settings (vscode, nvim) or in the graphql config file. (1) + - jump to definition in embedded files offset bug, for both fragments and code files with SDL strings + - cache invalidation for fragments (fragment lookup/autcoomplete data is more accurate, but incomplete/invalid fragments still do not autocomplete or validate, and remember fragment options always filter/validate by the `on` type!) + - schema cache invalidation for schema files - schema updates as you change the SDL files, and the generated file for code first by the `schemaCacheTTL` setting + - schema definition lookups & autocomplete crossing over into the wrong project + + **Notes** + + 1. If possible, configuring for your locally running framework or a schema registry client to handle schema updates and output to a `schema.graphql` or `introspection.json` will always provide a better experience. many graphql frameworks have this built in! Otherwise, we must use this new lazy polling approach if you provide a url schema (this includes both introspection URLs and remote file URLs, and the combination of these). + + ### Known Bugs Fixed + + - #3318 + - #2357 + - #3469 + - #2422 + - #2820 + - many more! + + ### Test Improvements + + - new, high level integration spec suite for the LSP with a matching test utility + - more unit test coverage + - **total increased test coverage of about 25% in the LSP server codebase.** + - many "happy paths" covered for both schema and code first contexts + - many bugs revealed (and their source) + + ### What's next? + + Another stage of the rewrite is already almost ready. This will fix even more bugs and improve memory usage, eliminate redundant parsing and ensure that graphql config's loaders do _all_ of the parsing and heavy lifting, thus honoring all the configs as well. It also significantly reduces the code complexity. + + There is also a plan to match Relay LSP's lookup config for either IDE (vscode, nvm, etc) settings as they provide, or by loading modules into your `graphql-config`! + +### Patch Changes + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Fixes several issues with Type System (SDL) completion across the ecosystem: + + - restores completion for object and input type fields when the document context is not detectable or parseable + - correct top-level completions for either of the unknown, type system or executable definitions. this leads to mixed top level completions when the document is unparseable, but now you are not seemingly restricted to only executable top level definitions + - `.graphqls` ad-hoc standard functionality remains, but is not required, as it is not part of the official spec, and the spec also allows mixed mode documents in theory, and this concept is required when the type is unknown + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Introduce `locateCommand` based on Relay LSP `pathToLocateCommand`: + + Now with `.extensions.languageService.locateCommand`, you can specify either the [existing signature](https://marketplace.visualstudio.com/items?itemName=meta.relay#relay.pathtolocatecommand-default-null) for relay, with the same callback parameters and return signature (of a string delimited by `:` characters), or you can return an object with {uri, range} for the exact set of coordinates for the destination range. the function can be sync or async. + + Relay LSP currently supports `Type` and `Type.field` for the 2nd argument. Ours also returns `Type.field(argument)` as a point of reference. It works with object types, input types, fragments, executable definitions and their fields, and should work for directive definitions as well. + + In the case of unnamed types such as fragment spreads, they return the name of the implemented type currently, but I'm curious what users prefer here. I assumed that some people may want to not be limited to only using this for SDL type definition lookups. Also look soon to see `locateCommand` support added for symbols, outline, and coming references and implementations. + + The module at the path you specify in relay LSP for `pathToLocateCommand` should work as such. + + ```ts + // import it + import { locateCommand } from './graphql/tooling/lsp/locate.js'; + export default { + languageService: { + locateCommand, + }, + + projects: { + a: { + schema: 'https://localhost:8000/graphql', + documents: './a/**/*.{ts,tsx,jsx,js,graphql}', + }, + b: { + schema: './schema/ascode.ts', + documents: './b/**/*.{ts,tsx,jsx,js,graphql}', + }, + }, + }; + ``` + + ```ts + // or define it inline + + import { type LocateCommand } from 'graphql-language-service-server'; + + // relay LSP style + const locateCommand = (projectName: string, typePath: string) => { + const { path, startLine, endLine } = ourLookupUtility( + projectName, + typePath, + ); + return `${path}:${startLine}:${endLine}`; + }; + + // an example with our alternative return signature + const locateCommand: LocateCommand = (projectName, typePath, info) => { + // pass more info, such as GraphQLType with the ast node. info.project is also available if you need it + const { path, range } = ourLookupUtility( + projectName, + typePath, + info.type.node, + ); + return { uri: path, range }; // range.start.line/range.end.line + }; + + export default { + languageService: { + locateCommand, + }, + schema: 'https://localhost:8000/graphql', + documents: './**/*.{ts,tsx,jsx,js,graphql}', + }; + ``` + + Passing a string as a module path to resolve is coming in a follow-up release. Then it can be used with `.yml`, `.toml`, `.json`, `package.json#graphql`, etc + + For now this was a quick baseline for a feature asked for in multiple channels! + + Let us know how this works, and about any other interoperability improvements between our graphql LSP and other language servers (relay, intellij, etc) used by you and colleauges in your engineering organisations. We are trying our best to keep up with the awesome innovations they have 👀! + +- Updated dependencies [[`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d), [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d), [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d)]: + - graphql-language-service-server@2.13.0 + - graphql-language-service@5.2.1 + ## 3.3.33 ### Patch Changes diff --git a/packages/graphql-language-service-cli/README.md b/packages/graphql-language-service-cli/README.md index b36a7adbe01..6114cbeb7b6 100644 --- a/packages/graphql-language-service-cli/README.md +++ b/packages/graphql-language-service-cli/README.md @@ -53,7 +53,7 @@ yarn global add graphql-language-service-cli with `npm`: ```sh -npm i -g graphql-language-service-cli +npm install -g graphql-language-service-cli ``` either will install the `graphql-lsp` bin globally diff --git a/packages/graphql-language-service-cli/package.json b/packages/graphql-language-service-cli/package.json index 01983e5cf36..47be9dc6dff 100644 --- a/packages/graphql-language-service-cli/package.json +++ b/packages/graphql-language-service-cli/package.json @@ -1,6 +1,6 @@ { "name": "graphql-language-service-cli", - "version": "3.3.33", + "version": "3.5.0", "description": "An interface for building GraphQL language services for IDEs", "contributors": [ "Hyohyeon Jeong ", @@ -32,16 +32,16 @@ "LSP" ], "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2" }, "dependencies": { "@babel/polyfill": "^7.12.1", "@types/yargs": "16.0.5", - "graphql-language-service": "^5.2.0", - "graphql-language-service-server": "^2.12.0", + "graphql-language-service": "^5.3.0", + "graphql-language-service-server": "^2.14.0", "yargs": "^16.2.0" }, "devDependencies": { - "graphql": "^16.8.1" + "graphql": "^17.0.0-alpha.7" } } diff --git a/packages/graphql-language-service-server/CHANGELOG.md b/packages/graphql-language-service-server/CHANGELOG.md index 52f894a5908..39db56e2790 100644 --- a/packages/graphql-language-service-server/CHANGELOG.md +++ b/packages/graphql-language-service-server/CHANGELOG.md @@ -1,5 +1,185 @@ # graphql-language-service-server +## 2.14.0 + +### Minor Changes + +- [#3682](https://github.com/graphql/graphiql/pull/3682) [`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931) Thanks [@yaacovCR](https://github.com/yaacovCR)! - Support v17 of `graphql-js` from `17.0.0-alpha.2` forward. + + Includes support for the latest incremental delivery response format. For further details, see https://github.com/graphql/defer-stream-wg/discussions/69. + +### Patch Changes + +- Updated dependencies [[`6c9f0df`](https://github.com/graphql/graphiql/commit/6c9f0df83ea4afe7fa59f84d83d59fba73dc3931)]: + - graphql-language-service@5.3.0 + +## 2.13.3 + +### Patch Changes + +- [#3703](https://github.com/graphql/graphiql/pull/3703) [`590a987`](https://github.com/graphql/graphiql/commit/590a987811b5ddcc37f5b76900dd2b8dba802f91) Thanks [@acao](https://github.com/acao)! - use `filepath` instead of `filePath` for `graphql-config` + +## 2.13.2 + +### Patch Changes + +- [#3647](https://github.com/graphql/graphiql/pull/3647) [`ba5720b`](https://github.com/graphql/graphiql/commit/ba5720b430ed1c888ff64c67aa4b9a36083b9ed0) Thanks [@acao](https://github.com/acao)! - several LSP fixes and improvements: + + **Bugfixes** + + debounce schema change events to fix codegen bugs to fix #3622 + + on mass file changes, network schema is overfetching because the schema cache is now invalidated on every watched schema file change + + to address this, we debounce the new `onSchemaChange` event by 400ms + + note that `schemaCacheTTL` can only be set in extension settings or graphql config at the top level - it will be ignored if configured per-project in the graphql config + + **Code Improvements** + + - Fixes flaky tests, and `schemaCacheTTL` setting not being passed to the cache + - Adds a test to validate network schema changes are reflected in the cache + +- [#3637](https://github.com/graphql/graphiql/pull/3637) [`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - update eslint plugins and fix errors + +- [#3671](https://github.com/graphql/graphiql/pull/3671) [`e2c04c7`](https://github.com/graphql/graphiql/commit/e2c04c7c2dc5109ff0446d9a6a010ffdffed1e44) Thanks [@acao](https://github.com/acao)! - - switch to using just @astrojs/compiler instead of the more complex "sync" adaptation using workers + - upgrade vue SFC parser to use the new reccomended import from vue package itself + - fix prettier config related to prettier & format on save for parseDocument tests + - fix jest/babel config related to some of the parsers +- Updated dependencies [[`fdec377`](https://github.com/graphql/graphiql/commit/fdec377f28ac0d918a219b78dfa2d8f0996ff84d)]: + - graphql-language-service@5.2.2 + +## 2.13.1 + +### Patch Changes + +- [#3628](https://github.com/graphql/graphiql/pull/3628) [`7fad662f`](https://github.com/graphql/graphiql/commit/7fad662f77eae9f842bb55cb93cb98df33bbc1ed) Thanks [@acao](https://github.com/acao)! - fix the lsp stream interface for stdin/out (neovim, etc) + +## 2.13.0 + +### Minor Changes + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Fix many schema and fragment lifecycle issues, not all of them, but many related to cacheing. Note: this makes `cacheSchemaForLookup` enabled by default again for schema first contexts. + + This fixes multiple cacheing bugs, upon addomg some in-depth integration test coverage for the LSP server. It also solves several bugs regarding loading config types, and properly restarts the server and invalidates schema when there are config changes. + + ### Bugfix Summary + + - configurable polling updates for network and other code first schema configuration, set to a 30s interval by default. powered by `schemaCacheTTL` which can be configured in the IDE settings (vscode, nvim) or in the graphql config file. (1) + - jump to definition in embedded files offset bug, for both fragments and code files with SDL strings + - cache invalidation for fragments (fragment lookup/autcoomplete data is more accurate, but incomplete/invalid fragments still do not autocomplete or validate, and remember fragment options always filter/validate by the `on` type!) + - schema cache invalidation for schema files - schema updates as you change the SDL files, and the generated file for code first by the `schemaCacheTTL` setting + - schema definition lookups & autocomplete crossing over into the wrong project + + **Notes** + + 1. If possible, configuring for your locally running framework or a schema registry client to handle schema updates and output to a `schema.graphql` or `introspection.json` will always provide a better experience. many graphql frameworks have this built in! Otherwise, we must use this new lazy polling approach if you provide a url schema (this includes both introspection URLs and remote file URLs, and the combination of these). + + ### Known Bugs Fixed + + - #3318 + - #2357 + - #3469 + - #2422 + - #2820 + - many more! + + ### Test Improvements + + - new, high level integration spec suite for the LSP with a matching test utility + - more unit test coverage + - **total increased test coverage of about 25% in the LSP server codebase.** + - many "happy paths" covered for both schema and code first contexts + - many bugs revealed (and their source) + + ### What's next? + + Another stage of the rewrite is already almost ready. This will fix even more bugs and improve memory usage, eliminate redundant parsing and ensure that graphql config's loaders do _all_ of the parsing and heavy lifting, thus honoring all the configs as well. It also significantly reduces the code complexity. + + There is also a plan to match Relay LSP's lookup config for either IDE (vscode, nvm, etc) settings as they provide, or by loading modules into your `graphql-config`! + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Introduce `locateCommand` based on Relay LSP `pathToLocateCommand`: + + Now with `.extensions.languageService.locateCommand`, you can specify either the [existing signature](https://marketplace.visualstudio.com/items?itemName=meta.relay#relay.pathtolocatecommand-default-null) for relay, with the same callback parameters and return signature (of a string delimited by `:` characters), or you can return an object with {uri, range} for the exact set of coordinates for the destination range. the function can be sync or async. + + Relay LSP currently supports `Type` and `Type.field` for the 2nd argument. Ours also returns `Type.field(argument)` as a point of reference. It works with object types, input types, fragments, executable definitions and their fields, and should work for directive definitions as well. + + In the case of unnamed types such as fragment spreads, they return the name of the implemented type currently, but I'm curious what users prefer here. I assumed that some people may want to not be limited to only using this for SDL type definition lookups. Also look soon to see `locateCommand` support added for symbols, outline, and coming references and implementations. + + The module at the path you specify in relay LSP for `pathToLocateCommand` should work as such. + + ```ts + // import it + import { locateCommand } from './graphql/tooling/lsp/locate.js'; + export default { + languageService: { + locateCommand, + }, + + projects: { + a: { + schema: 'https://localhost:8000/graphql', + documents: './a/**/*.{ts,tsx,jsx,js,graphql}', + }, + b: { + schema: './schema/ascode.ts', + documents: './b/**/*.{ts,tsx,jsx,js,graphql}', + }, + }, + }; + ``` + + ```ts + // or define it inline + + import { type LocateCommand } from 'graphql-language-service-server'; + + // relay LSP style + const locateCommand = (projectName: string, typePath: string) => { + const { path, startLine, endLine } = ourLookupUtility( + projectName, + typePath, + ); + return `${path}:${startLine}:${endLine}`; + }; + + // an example with our alternative return signature + const locateCommand: LocateCommand = (projectName, typePath, info) => { + // pass more info, such as GraphQLType with the ast node. info.project is also available if you need it + const { path, range } = ourLookupUtility( + projectName, + typePath, + info.type.node, + ); + return { uri: path, range }; // range.start.line/range.end.line + }; + + export default { + languageService: { + locateCommand, + }, + schema: 'https://localhost:8000/graphql', + documents: './**/*.{ts,tsx,jsx,js,graphql}', + }; + ``` + + Passing a string as a module path to resolve is coming in a follow-up release. Then it can be used with `.yml`, `.toml`, `.json`, `package.json#graphql`, etc + + For now this was a quick baseline for a feature asked for in multiple channels! + + Let us know how this works, and about any other interoperability improvements between our graphql LSP and other language servers (relay, intellij, etc) used by you and colleauges in your engineering organisations. We are trying our best to keep up with the awesome innovations they have 👀! + +### Patch Changes + +- [#3521](https://github.com/graphql/graphiql/pull/3521) [`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d) Thanks [@acao](https://github.com/acao)! - Fixes several issues with Type System (SDL) completion across the ecosystem: + + - restores completion for object and input type fields when the document context is not detectable or parseable + - correct top-level completions for either of the unknown, type system or executable definitions. this leads to mixed top level completions when the document is unparseable, but now you are not seemingly restricted to only executable top level definitions + - `.graphqls` ad-hoc standard functionality remains, but is not required, as it is not part of the official spec, and the spec also allows mixed mode documents in theory, and this concept is required when the type is unknown + +- Updated dependencies [[`aa6dbbb4`](https://github.com/graphql/graphiql/commit/aa6dbbb45bf51c1966537640fbe5c4f375735c8d)]: + - graphql-language-service@5.2.1 + ## 2.12.0 ### Minor Changes diff --git a/packages/graphql-language-service-server/README.md b/packages/graphql-language-service-server/README.md index d0f5388cd9a..71ce3b3e9da 100644 --- a/packages/graphql-language-service-server/README.md +++ b/packages/graphql-language-service-server/README.md @@ -34,16 +34,15 @@ Supported features include: ### Dependencies -An LSP compatible client with its own file watcher, that sends watch -notifications to the server. - -**DROPPED**: GraphQL Language Service no longer depends on -[Watchman](https://facebook.github.io/watchman/) +- An LSP compatible client with its own file watcher, that sends watch + notifications to the server, such as vscode, nvim, or sublime-lsp. +- Node.js `^18.18.0 || >=20.9.0` or later is required. +- (for now) a graphql config file is required ### Installation ```bash -npm install --save graphql-language-service-server +npm install graphql-language-service-server # or yarn add graphql-language-service-server ``` @@ -123,6 +122,9 @@ further customization: ```ts import { loadConfig } from 'graphql-config'; // 3.0.0 or later! +// with required params +const config = await loadConfig(); + await startServer({ method: 'node', // or instead of configName, an exact path (relative from rootDir or absolute) @@ -131,7 +133,7 @@ await startServer({ // configDir: '', loadConfigOptions: { // any of the options for graphql-config@3 `loadConfig()` - + schema: await config.getSchema(), // rootDir is same as `configDir` before, the path where the graphql config file would be found by cosmic-config rootDir: 'config/', // or - the relative or absolute path to your file @@ -156,12 +158,31 @@ module.exports = { // a function that returns an array of validation rules, ala https://github.com/graphql/graphql-js/tree/main/src/validation/rules // note that this file will be loaded by the vscode runtime, so the node version and other factors will come into play customValidationRules: require('./config/customValidationRules'), + schemaCacheTTL: 1000, // reduce or increase minimum schema cache lifetime from 30000ms (30 seconds). you may want to reduce this if you are developing fullstack with network schema languageService: { - // should the language service read schema for definition lookups from a cached file based on graphql config output? + // this is enabled by default if non-local files are specified in the project `schema` // NOTE: this will disable all definition lookup for local SDL files cacheSchemaFileForLookup: true, // undefined by default which has the same effect as `true`, set to `false` if you are already using // `graphql-eslint` or some other tool for validating graphql in your IDE. Must be explicitly `false` to disable this feature, not just "falsy" enableValidation: true, + // (experimental) enhanced auto expansion of graphql leaf fields and arguments + fillLeafsOnComplete: true, + // instead of jumping directly to the SDL file, you can override definition peek/jump results to point to different files or locations + // (for example, source files for your schema in any language!) + // based on Relay vscode's pathToLocateCommand + // see LocateCommand type! + locateCommand(projectName, typePath, info) { + // pass more info, such as GraphQLType with the ast node. info.project is also available if you need it + const { path, range } = ourLookupUtility( + projectName, + typePath, + info.type.node, + ); + return { uri: path, range }; // range.start.line/range.end.character/etc, base 1 + // you can also return relay LSP style + // return '/path/to/file.py:20:23'; // (range: 20:1 ) + // return '/path/to/file.py'; // (range: 1:1 1:1) + }, }, }, }; @@ -237,14 +258,16 @@ via `initializationOptions` in nvim.coc. The options are mostly designed to configure graphql-config's load parameters, the only thing we can't configure with graphql config. The final option can be set in `graphql-config` as well -| Parameter | Default | Description | -| ----------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `graphql-config.load.baseDir` | workspace root or process.cwd() | the path where graphql config looks for config files | -| `graphql-config.load.filePath` | `null` | exact filepath of the config file. | -| `graphql-config.load.configName` | `graphql` | config name prefix instead of `graphql` | -| `graphql-config.load.legacy` | `true` | backwards compatibility with `graphql-config@2` | -| `graphql-config.dotEnvPath` | `null` | backwards compatibility with `graphql-config@2` | -| `vscode-graphql.cacheSchemaFileForLookup` | `false` | generate an SDL file based on your graphql-config schema configuration for schema definition lookup and other features. useful when your `schema` config are urls | +| Parameter | Default | Description | +| ----------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `graphql-config.load.baseDir` | workspace root or process.cwd() | the path where graphql config looks for config files | +| `graphql-config.load.filepath` | `null` | exact filepath of the config file. | +| `graphql-config.load.configName` | `graphql` | config name prefix instead of `graphql` | +| `graphql-config.load.legacy` | `true` | backwards compatibility with `graphql-config@2` | +| `graphql-config.dotEnvPath` | `null` | backwards compatibility with `graphql-config@2` | +| `vscode-graphql.cacheSchemaFileForLookup` | `true` if `schema` contains non-SDL files or URLs | generate an SDL file based on your graphql-config schema configuration for definition lookup and other features. enabled by default when your `schema` config are URLs or introspection JSON, or if you have any non-local SDL files in `schema` | +| `vscode-graphql.schemaCacheTTL` | `30000` | an integer value in milliseconds for the desired minimum cache lifetime for all schemas, which also causes the generated file to be re-written. set to 30s by default. effectively a "lazy" form of polling. if you are developing a schema alongside client queries, you may want to decrease this | +| `vscode-graphql.debug` | `false` | show more verbose log output in the output channel | all the `graphql-config.load.*` configuration values come from static `loadConfig()` options in graphql config. diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json index d4cd4eaceff..4f31e013381 100644 --- a/packages/graphql-language-service-server/package.json +++ b/packages/graphql-language-service-server/package.json @@ -1,6 +1,6 @@ { "name": "graphql-language-service-server", - "version": "2.12.0", + "version": "2.14.0", "description": "Server process backing the GraphQL Language Service", "contributors": [ "Greg Hurrell (https://greg.hurrell.net/)", @@ -34,39 +34,41 @@ "module": "esm/index.js", "typings": "esm/index.d.ts", "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2" }, - "COMMENT": "please do not remove depenencies without thorough testing. many dependencies are not imported directly, as they are peer dependencies", + "COMMENT": "please do not remove dependencies without thorough testing. many dependencies are not imported directly, as they are peer dependencies", "dependencies": { - "@astrojs/compiler": "^2.5.0", + "@astrojs/compiler": "^2.10.1", "@babel/parser": "^7.23.6", "@babel/types": "^7.23.5", "@graphql-tools/code-file-loader": "8.0.3", - "@vue/compiler-sfc": "^3.4.5", - "astrojs-compiler-sync": "^0.3.5", "cosmiconfig-toml-loader": "^1.0.0", "dotenv": "10.0.0", "fast-glob": "^3.2.7", "glob": "^7.2.0", "graphql-config": "5.0.3", - "graphql-language-service": "^5.2.0", + "graphql-language-service": "^5.3.0", + "lru-cache": "^10.2.0", "mkdirp": "^1.0.4", "node-abort-controller": "^3.0.1", "nullthrows": "^1.0.0", "source-map-js": "1.0.2", "svelte": "^4.1.1", "svelte2tsx": "^0.7.0", + "typescript": "^5.3.3", "vscode-jsonrpc": "^8.0.1", "vscode-languageserver": "^8.0.1", "vscode-languageserver-types": "^3.17.2", "vscode-uri": "^3.0.2", - "typescript": "^5.3.3" + "vue": "^3.2.0" }, "devDependencies": { "@types/glob": "^8.1.0", "@types/mkdirp": "^1.0.1", + "@types/mock-fs": "^4.13.4", "cross-env": "^7.0.2", - "graphql": "^16.8.1", + "debounce-promise": "^3.1.2", + "graphql": "^17.0.0-alpha.7", "mock-fs": "^5.2.0" } } diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts index f7b043e5676..e8c3e880e19 100644 --- a/packages/graphql-language-service-server/src/GraphQLCache.ts +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -20,7 +20,6 @@ import { } from 'graphql'; import type { CachedContent, - GraphQLCache as GraphQLCacheInterface, GraphQLFileMetadata, GraphQLFileInfo, FragmentInfo, @@ -46,18 +45,30 @@ import stringToHash from './stringToHash'; import glob from 'glob'; import { LoadConfigOptions } from './types'; import { URI } from 'vscode-uri'; -import { CodeFileLoader } from '@graphql-tools/code-file-loader'; +import { + CodeFileLoader, + CodeFileLoaderConfig, +} from '@graphql-tools/code-file-loader'; import { DEFAULT_SUPPORTED_EXTENSIONS, DEFAULT_SUPPORTED_GRAPHQL_EXTENSIONS, } from './constants'; import { NoopLogger, Logger } from './Logger'; +import { LRUCache } from 'lru-cache'; +// import { is } from '@babel/types'; + +const codeLoaderConfig: CodeFileLoaderConfig = { + noSilentErrors: false, + pluckConfig: { + skipIndent: true, + }, +}; const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { // For schema - api.loaders.schema.register(new CodeFileLoader()); + api.loaders.schema.register(new CodeFileLoader(codeLoaderConfig)); // For documents - api.loaders.documents.register(new CodeFileLoader()); + api.loaders.documents.register(new CodeFileLoader(codeLoaderConfig)); return { name: 'languageService' }; }; @@ -65,16 +76,22 @@ const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { // Maximum files to read when processing GraphQL files. const MAX_READS = 200; +export type OnSchemaChange = (project: GraphQLProjectConfig) => void; + export async function getGraphQLCache({ parser, logger, loadConfigOptions, config, + onSchemaChange, + schemaCacheTTL, }: { parser: typeof parseDocument; logger: Logger | NoopLogger; loadConfigOptions: LoadConfigOptions; config?: GraphQLConfig; + onSchemaChange?: OnSchemaChange; + schemaCacheTTL?: number; }): Promise { const graphQLConfig = config || @@ -90,10 +107,15 @@ export async function getGraphQLCache({ config: graphQLConfig!, parser, logger, + onSchemaChange, + schemaCacheTTL: + schemaCacheTTL ?? + // @ts-expect-error TODO: add types for extension configs + config?.extensions?.get('languageService')?.schemaCacheTTL, }); } -export class GraphQLCache implements GraphQLCacheInterface { +export class GraphQLCache { _configDir: Uri; _graphQLFileListCache: Map>; _graphQLConfig: GraphQLConfig; @@ -103,34 +125,54 @@ export class GraphQLCache implements GraphQLCacheInterface { _typeDefinitionsCache: Map>; _parser: typeof parseDocument; _logger: Logger | NoopLogger; + _onSchemaChange?: OnSchemaChange; + _schemaCacheTTL?: number; constructor({ configDir, config, parser, logger, + onSchemaChange, + schemaCacheTTL, }: { configDir: Uri; config: GraphQLConfig; parser: typeof parseDocument; logger: Logger | NoopLogger; + onSchemaChange?: OnSchemaChange; + schemaCacheTTL?: number; }) { this._configDir = configDir; this._graphQLConfig = config; this._graphQLFileListCache = new Map(); - this._schemaMap = new Map(); + this._schemaMap = new LRUCache({ + max: 20, + ttl: schemaCacheTTL ?? 1000 * 30, + ttlAutopurge: true, + updateAgeOnGet: false, + }); this._fragmentDefinitionsCache = new Map(); this._typeDefinitionsCache = new Map(); this._typeExtensionMap = new Map(); this._parser = parser; this._logger = logger; + this._onSchemaChange = onSchemaChange; } getGraphQLConfig = (): GraphQLConfig => this._graphQLConfig; getProjectForFile = (uri: string): GraphQLProjectConfig | void => { try { - return this._graphQLConfig.getProjectForFile(URI.parse(uri).fsPath); + const project = this._graphQLConfig.getProjectForFile( + URI.parse(uri).fsPath, + ); + if (!project.documents) { + this._logger.warn( + `No documents configured for project ${project.name}. Many features will not work correctly.`, + ); + } + return project; } catch (err) { this._logger.error( `there was an error loading the project config for this file ${err}`, @@ -236,29 +278,6 @@ export class GraphQLCache implements GraphQLCacheInterface { return fragmentDefinitions; }; - getObjectTypeDependencies = async ( - query: string, - objectTypeDefinitions?: Map, - ): Promise> => { - // If there isn't context for object type references, - // return an empty array. - if (!objectTypeDefinitions) { - return []; - } - // If the query cannot be parsed, validations cannot happen yet. - // Return an empty array. - let parsedQuery; - try { - parsedQuery = parse(query); - } catch { - return []; - } - return this.getObjectTypeDependenciesForAST( - parsedQuery, - objectTypeDefinitions, - ); - }; - getObjectTypeDependenciesForAST = async ( parsedQuery: ASTNode, objectTypeDefinitions: Map, @@ -424,38 +443,12 @@ export class GraphQLCache implements GraphQLCacheInterface { return patterns; }; - async _updateGraphQLFileListCache( - graphQLFileMap: Map, - metrics: { size: number; mtime: number }, - filePath: Uri, - exists: boolean, - ): Promise> { - const fileAndContent = exists - ? await this.promiseToReadGraphQLFile(filePath) - : null; - - const existingFile = graphQLFileMap.get(filePath); - - // 3 cases for the cache invalidation: create/modify/delete. - // For create/modify, swap the existing entry if available; - // otherwise, just push in the new entry created. - // For delete, check `exists` and splice the file out. - if (existingFile && !exists) { - graphQLFileMap.delete(filePath); - } else if (fileAndContent) { - const graphQLFileInfo = { ...fileAndContent, ...metrics }; - graphQLFileMap.set(filePath, graphQLFileInfo); - } - - return graphQLFileMap; - } - async updateFragmentDefinition( - rootDir: Uri, + projectCacheKey: Uri, filePath: Uri, contents: Array, ): Promise { - const cache = this._fragmentDefinitionsCache.get(rootDir); + const cache = this._fragmentDefinitionsCache.get(projectCacheKey); const asts = contents.map(({ query }) => { try { return { @@ -473,55 +466,44 @@ export class GraphQLCache implements GraphQLCacheInterface { cache.delete(key); } } - for (const { ast, query } of asts) { - if (!ast) { - continue; - } - for (const definition of ast.definitions) { - if (definition.kind === Kind.FRAGMENT_DEFINITION) { - cache.set(definition.name.value, { - filePath, - content: query, - definition, - }); - } - } - } + this._setFragmentCache(asts, cache, filePath); + } else { + const newFragmentCache = this._setFragmentCache( + asts, + new Map(), + filePath, + ); + this._fragmentDefinitionsCache.set(projectCacheKey, newFragmentCache); } } - - async updateFragmentDefinitionCache( - rootDir: Uri, - filePath: Uri, - exists: boolean, - ): Promise { - const fileAndContent = exists - ? await this.promiseToReadGraphQLFile(filePath) - : null; - // In the case of fragment definitions, the cache could just map the - // definition name to the parsed ast, whether or not it existed - // previously. - // For delete, remove the entry from the set. - if (!exists) { - const cache = this._fragmentDefinitionsCache.get(rootDir); - if (cache) { - cache.delete(filePath); + _setFragmentCache( + asts: { ast: DocumentNode | null; query: string }[], + fragmentCache: Map, + filePath: string | undefined, + ) { + for (const { ast, query } of asts) { + if (!ast) { + continue; + } + for (const definition of ast.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + fragmentCache.set(definition.name.value, { + filePath, + content: query, + definition, + }); + } } - } else if (fileAndContent?.queries) { - await this.updateFragmentDefinition( - rootDir, - filePath, - fileAndContent.queries, - ); } + return fragmentCache; } async updateObjectTypeDefinition( - rootDir: Uri, + projectCacheKey: Uri, filePath: Uri, contents: Array, ): Promise { - const cache = this._typeDefinitionsCache.get(rootDir); + const cache = this._typeDefinitionsCache.get(projectCacheKey); const asts = contents.map(({ query }) => { try { return { @@ -539,47 +521,32 @@ export class GraphQLCache implements GraphQLCacheInterface { cache.delete(key); } } - for (const { ast, query } of asts) { - if (!ast) { - continue; - } - for (const definition of ast.definitions) { - if (isTypeDefinitionNode(definition)) { - cache.set(definition.name.value, { - filePath, - content: query, - definition, - }); - } - } - } + this._setDefinitionCache(asts, cache, filePath); + } else { + const newTypeCache = this._setDefinitionCache(asts, new Map(), filePath); + this._typeDefinitionsCache.set(projectCacheKey, newTypeCache); } } - - async updateObjectTypeDefinitionCache( - rootDir: Uri, - filePath: Uri, - exists: boolean, - ): Promise { - const fileAndContent = exists - ? await this.promiseToReadGraphQLFile(filePath) - : null; - // In the case of type definitions, the cache could just map the - // definition name to the parsed ast, whether or not it existed - // previously. - // For delete, remove the entry from the set. - if (!exists) { - const cache = this._typeDefinitionsCache.get(rootDir); - if (cache) { - cache.delete(filePath); + _setDefinitionCache( + asts: { ast: DocumentNode | null; query: string }[], + typeCache: Map, + filePath: string | undefined, + ) { + for (const { ast, query } of asts) { + if (!ast) { + continue; + } + for (const definition of ast.definitions) { + if (isTypeDefinitionNode(definition)) { + typeCache.set(definition.name.value, { + filePath, + content: query, + definition, + }); + } } - } else if (fileAndContent?.queries) { - await this.updateObjectTypeDefinition( - rootDir, - filePath, - fileAndContent.queries, - ); } + return typeCache; } _extendSchema( @@ -693,6 +660,9 @@ export class GraphQLCache implements GraphQLCacheInterface { if (schemaCacheKey) { this._schemaMap.set(schemaCacheKey, schema); + if (this._onSchemaChange) { + this._onSchemaChange(projectConfig); + } } return schema; }; @@ -713,7 +683,7 @@ export class GraphQLCache implements GraphQLCacheInterface { } _getProjectName(projectConfig: GraphQLProjectConfig) { - return projectConfig || 'default'; + return projectConfig?.name || 'default'; } /** @@ -825,13 +795,13 @@ export class GraphQLCache implements GraphQLCacheInterface { promiseToReadGraphQLFile = async ( filePath: Uri, ): Promise => { - const content = await readFile(URI.parse(filePath).fsPath, 'utf8'); + const content = await readFile(URI.parse(filePath).fsPath, 'utf-8'); const asts: DocumentNode[] = []; let queries: CachedContent[] = []; if (content.trim().length !== 0) { try { - queries = this._parser( + queries = await this._parser( content, filePath, DEFAULT_SUPPORTED_EXTENSIONS, diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts index aeaa4c92a8e..5e307daf990 100644 --- a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -20,6 +20,8 @@ import { parse, print, isTypeDefinitionNode, + ArgumentNode, + typeFromAST, } from 'graphql'; import { @@ -29,7 +31,6 @@ import { IPosition, Outline, OutlineTree, - GraphQLCache, getAutocompleteSuggestions, getHoverInformation, HoverConfig, @@ -41,12 +42,15 @@ import { getDefinitionQueryResultForDefinitionNode, getDefinitionQueryResultForNamedType, getDefinitionQueryResultForField, - DefinitionQueryResult, getASTNodeAtPosition, getTokenAtPosition, getTypeInfo, + DefinitionQueryResponse, + getDefinitionQueryResultForArgument, } from 'graphql-language-service'; +import type { GraphQLCache } from './GraphQLCache'; + import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config'; import type { Logger } from 'vscode-languageserver'; @@ -163,9 +167,8 @@ export class GraphQLLanguageService { // If there's a matching config, proceed to prepare to run validation let source = document; - const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( - projectConfig, - ); + const fragmentDefinitions = + await this._graphQLCache.getFragmentDefinitions(projectConfig); const fragmentDependencies = await this._graphQLCache.getFragmentDependencies( @@ -198,8 +201,6 @@ export class GraphQLLanguageService { typeof extensions.customValidationRules === 'function' ) { customRules = extensions.customValidationRules(this._graphQLConfig); - - /* eslint-enable no-implicit-coercion */ } const schema = await this._graphQLCache.getSchema( projectName, @@ -223,30 +224,31 @@ export class GraphQLLanguageService { return []; } const schema = await this._graphQLCache.getSchema(projectConfig.name); - const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( - projectConfig, - ); + if (!schema) { + return []; + } + let fragmentInfo = [] as Array; + try { + const fragmentDefinitions = + await this._graphQLCache.getFragmentDefinitions(projectConfig); + fragmentInfo = Array.from(fragmentDefinitions).map( + ([, info]) => info.definition, + ); + } catch {} - const fragmentInfo = Array.from(fragmentDefinitions).map( - ([, info]) => info.definition, + return getAutocompleteSuggestions( + schema, + query, + position, + undefined, + fragmentInfo, + { + uri: filePath, + fillLeafsOnComplete: + projectConfig?.extensions?.languageService?.fillLeafsOnComplete ?? + false, + }, ); - - if (schema) { - return getAutocompleteSuggestions( - schema, - query, - position, - undefined, - fragmentInfo, - { - uri: filePath, - fillLeafsOnComplete: - projectConfig?.extensions?.languageService?.fillLeafsOnComplete ?? - false, - }, - ); - } - return []; } public async getHoverInformation( @@ -271,12 +273,17 @@ export class GraphQLLanguageService { query: string, position: IPosition, filePath: Uri, - ): Promise { + ): Promise { const projectConfig = this.getConfigForURI(filePath); + if (!projectConfig) { return null; } + const schema = await this._graphQLCache.getSchema(projectConfig.name); + if (!schema) { + return null; + } let ast; try { ast = parse(query); @@ -285,36 +292,40 @@ export class GraphQLLanguageService { } const node = getASTNodeAtPosition(query, ast, position); + // @ts-expect-error + const type = node && typeFromAST(schema, node); + + let queryResult: DefinitionQueryResponse | null = null; if (node) { switch (node.kind) { case Kind.FRAGMENT_SPREAD: - return this._getDefinitionForFragmentSpread( + queryResult = await this._getDefinitionForFragmentSpread( query, ast, node, filePath, projectConfig, ); - + break; case Kind.FRAGMENT_DEFINITION: case Kind.OPERATION_DEFINITION: - return getDefinitionQueryResultForDefinitionNode( + queryResult = getDefinitionQueryResultForDefinitionNode( filePath, query, node, ); - + break; case Kind.NAMED_TYPE: - return this._getDefinitionForNamedType( + queryResult = await this._getDefinitionForNamedType( query, ast, node, filePath, projectConfig, ); - + break; case Kind.FIELD: - return this._getDefinitionForField( + queryResult = await this._getDefinitionForField( query, ast, node, @@ -322,8 +333,26 @@ export class GraphQLLanguageService { projectConfig, position, ); + break; + case Kind.ARGUMENT: + queryResult = await this._getDefinitionForArgument( + query, + ast, + node, + filePath, + projectConfig, + position, + ); + break; } } + if (queryResult) { + return { + ...queryResult, + node, + type, + }; + } return null; } @@ -382,7 +411,7 @@ export class GraphQLLanguageService { node: NamedTypeNode, filePath: Uri, projectConfig: GraphQLProjectConfig, - ): Promise { + ): Promise { const objectTypeDefinitions = await this._graphQLCache.getObjectTypeDefinitions(projectConfig); @@ -400,13 +429,11 @@ export class GraphQLLanguageService { definition, })); - const result = await getDefinitionQueryResultForNamedType( + return getDefinitionQueryResultForNamedType( query, node, dependencies.concat(localOperationDefinitionInfos), ); - - return result; } async _getDefinitionForField( @@ -432,13 +459,44 @@ export class GraphQLLanguageService { // TODO: need something like getObjectTypeDependenciesForAST? const dependencies = [...objectTypeDefinitions.values()]; - const result = await getDefinitionQueryResultForField( + return getDefinitionQueryResultForField( fieldName, parentTypeName, dependencies, ); + } + + return null; + } + + async _getDefinitionForArgument( + query: string, + _ast: DocumentNode, + _node: ArgumentNode, + _filePath: Uri, + projectConfig: GraphQLProjectConfig, + position: IPosition, + ) { + const token = getTokenAtPosition(query, position); + const schema = await this._graphQLCache.getSchema(projectConfig.name); + + const typeInfo = getTypeInfo(schema!, token.state); + const fieldName = typeInfo.fieldDef?.name; + const argumentName = typeInfo.argDef?.name; + if (typeInfo && fieldName && argumentName) { + const objectTypeDefinitions = + await this._graphQLCache.getObjectTypeDefinitions(projectConfig); - return result; + // TODO: need something like getObjectTypeDependenciesForAST? + const dependencies = [...objectTypeDefinitions.values()]; + + return getDefinitionQueryResultForArgument( + argumentName, + fieldName, + // @ts-expect-error - typeInfo is not typed correctly + typeInfo.argDef?.type?.name, + dependencies, + ); } return null; @@ -450,10 +508,9 @@ export class GraphQLLanguageService { node: FragmentSpreadNode, filePath: Uri, projectConfig: GraphQLProjectConfig, - ): Promise { - const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( - projectConfig, - ); + ): Promise { + const fragmentDefinitions = + await this._graphQLCache.getFragmentDefinitions(projectConfig); const dependencies = await this._graphQLCache.getFragmentDependenciesForAST( ast, @@ -475,14 +532,13 @@ export class GraphQLLanguageService { }), ); - const result = await getDefinitionQueryResultForFragmentSpread( + return getDefinitionQueryResultForFragmentSpread( query, node, dependencies.concat(localFragInfos), ); - - return result; } + async getOutline(documentText: string): Promise { return getOutline(documentText); } diff --git a/packages/graphql-language-service-server/src/Logger.ts b/packages/graphql-language-service-server/src/Logger.ts index ccc58defa81..85f530f1fd0 100644 --- a/packages/graphql-language-service-server/src/Logger.ts +++ b/packages/graphql-language-service-server/src/Logger.ts @@ -11,7 +11,15 @@ import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; import { Connection } from 'vscode-languageserver'; export class Logger implements VSCodeLogger { - constructor(private _connection: Connection) {} + // TODO: allow specifying exact log level? + // for now this is to handle the debug setting + private logLevel: number; + constructor( + private _connection: Connection, + debug?: boolean, + ) { + this.logLevel = debug ? 1 : 0; + } error(message: string): void { this._connection.console.error(message); @@ -26,7 +34,15 @@ export class Logger implements VSCodeLogger { } log(message: string): void { - this._connection.console.log(message); + if (this.logLevel > 0) { + this._connection.console.log(message); + } + } + set level(level: number) { + this.logLevel = level; + } + get level() { + return this.logLevel; } } @@ -35,4 +51,8 @@ export class NoopLogger implements VSCodeLogger { warn() {} info() {} log() {} + set level(_level: number) {} + get level() { + return 0; + } } diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts index e871ba4e340..817e178c237 100644 --- a/packages/graphql-language-service-server/src/MessageProcessor.ts +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -7,11 +7,9 @@ * */ -import mkdirp from 'mkdirp'; -import { readFileSync, existsSync, writeFileSync } from 'node:fs'; -import { writeFile } from 'node:fs/promises'; +import { existsSync, mkdirSync } from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; -import glob from 'fast-glob'; import { URI } from 'vscode-uri'; import { CachedContent, @@ -34,7 +32,6 @@ import type { DidOpenTextDocumentParams, DidChangeConfigurationParams, Diagnostic, - CompletionItem, CompletionList, CancellationToken, Hover, @@ -66,16 +63,19 @@ import { ConfigEmptyError, ConfigInvalidError, ConfigNotFoundError, - GraphQLExtensionDeclaration, LoaderNoResultError, ProjectNotFoundError, } from 'graphql-config'; -import type { LoadConfigOptions } from './types'; +import type { LoadConfigOptions, LocateCommand } from './types'; import { DEFAULT_SUPPORTED_EXTENSIONS, SupportedExtensionsEnum, } from './constants'; import { NoopLogger, Logger } from './Logger'; +import glob from 'fast-glob'; +import { isProjectSDLOnly, unwrapProjectSchema } from './common'; +import { DefinitionQueryResponse } from 'graphql-language-service/src/interface'; +import { default as debounce } from 'debounce-promise'; const configDocLink = 'https://www.npmjs.com/package/graphql-language-service-server#user-content-graphql-configuration-file'; @@ -84,29 +84,27 @@ type CachedDocumentType = { version: number; contents: CachedContent[]; }; + function toPosition(position: VscodePosition): IPosition { return new Position(position.line, position.character); } export class MessageProcessor { - _connection: Connection; - _graphQLCache!: GraphQLCache; - _graphQLConfig: GraphQLConfig | undefined; - _languageService!: GraphQLLanguageService; - _textDocumentCache = new Map(); - _isInitialized = false; - _isGraphQLConfigMissing: boolean | null = null; - _willShutdown = false; - _logger: Logger | NoopLogger; - _extensions?: GraphQLExtensionDeclaration[]; - _parser: (text: string, uri: string) => CachedContent[]; - _tmpDir: string; - _tmpUriBase: string; - _tmpDirBase: string; - _loadConfigOptions: LoadConfigOptions; - _schemaCacheInit = false; - _rootPath: string = process.cwd(); - _settings: any; + private _connection: Connection; + private _graphQLCache!: GraphQLCache; + private _languageService!: GraphQLLanguageService; + private _textDocumentCache = new Map(); + private _isInitialized = false; + private _isGraphQLConfigMissing: boolean | null = null; + private _willShutdown = false; + private _logger: Logger | NoopLogger; + private _parser: (text: string, uri: string) => Promise; + private _tmpDir: string; + private _tmpDirBase: string; + private _loadConfigOptions: LoadConfigOptions; + private _rootPath: string = process.cwd(); + private _settings: any; + private _providedConfig?: GraphQLConfig; constructor({ logger, @@ -127,27 +125,22 @@ export class MessageProcessor { tmpDir?: string; connection: Connection; }) { + if (config) { + this._providedConfig = config; + } this._connection = connection; this._logger = logger; - this._graphQLConfig = config; - this._parser = (text, uri) => { + this._parser = async (text, uri) => { const p = parser ?? parseDocument; return p(text, uri, fileExtensions, graphqlFileExtensions, this._logger); }; this._tmpDir = tmpDir || tmpdir(); this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service'); - this._tmpUriBase = URI.file(this._tmpDirBase).toString(); // use legacy mode by default for backwards compatibility this._loadConfigOptions = { legacy: true, ...loadConfigOptions }; - if ( - loadConfigOptions.extensions && - loadConfigOptions.extensions?.length > 0 - ) { - this._extensions = loadConfigOptions.extensions; - } if (!existsSync(this._tmpDirBase)) { - void mkdirp(this._tmpDirBase); + void mkdirSync(this._tmpDirBase); } } get connection(): Connection { @@ -157,7 +150,7 @@ export class MessageProcessor { this._connection = connection; } - async handleInitializeRequest( + public async handleInitializeRequest( params: InitializeParams, _token?: CancellationToken, configDir?: string, @@ -172,7 +165,7 @@ export class MessageProcessor { documentSymbolProvider: true, completionProvider: { resolveProvider: true, - triggerCharacters: [' ', ':', '$', '(', '@'], + triggerCharacters: [' ', ':', '$', '(', '@', '\n'], }, definitionProvider: true, textDocumentSync: 1, @@ -194,9 +187,6 @@ export class MessageProcessor { 'no rootPath configured in extension or server, defaulting to cwd', ); } - if (!serverCapabilities) { - throw new Error('GraphQL Language Server is not initialized.'); - } this._logger.info( JSON.stringify({ @@ -207,8 +197,8 @@ export class MessageProcessor { return serverCapabilities; } - - async _updateGraphQLConfig() { + // TODO next: refactor (most of) this into the `GraphQLCache` class + async _initializeGraphQLCaches() { const settings = await this._connection.workspace.getConfiguration({ section: 'graphql-config', }); @@ -216,13 +206,18 @@ export class MessageProcessor { const vscodeSettings = await this._connection.workspace.getConfiguration({ section: 'vscode-graphql', }); - if (settings?.dotEnvPath) { - require('dotenv').config({ path: settings.dotEnvPath }); - } + + // TODO: eventually we will instantiate an instance of this per workspace, + // so rootDir should become that workspace's rootDir this._settings = { ...settings, ...vscodeSettings }; const rootDir = this._settings?.load?.rootDir.length ? this._settings?.load?.rootDir : this._rootPath; + if (settings?.dotEnvPath) { + require('dotenv').config({ + path: path.resolve(rootDir, settings.dotEnvPath), + }); + } this._rootPath = rootDir; this._loadConfigOptions = { ...Object.keys(this._settings?.load ?? {}).reduce((agg, key) => { @@ -234,30 +229,70 @@ export class MessageProcessor { }, this._settings.load ?? {}), rootDir, }; - try { - // reload the graphql cache - this._graphQLCache = await getGraphQLCache({ - parser: this._parser, - loadConfigOptions: this._loadConfigOptions, - logger: this._logger, - }); - this._languageService = new GraphQLLanguageService( - this._graphQLCache, - this._logger, - ); - if (this._graphQLConfig || this._graphQLCache?.getGraphQLConfig) { - const config = - this._graphQLConfig ?? this._graphQLCache.getGraphQLConfig(); + const onSchemaChange = debounce(async (project: GraphQLProjectConfig) => { + const { cacheSchemaFileForLookup } = + this.getCachedSchemaSettings(project); + if (!cacheSchemaFileForLookup) { + return; + } + const unwrappedSchema = unwrapProjectSchema(project); + const sdlOnly = isProjectSDLOnly(unwrappedSchema); + if (sdlOnly) { + return; + } + return this.cacheConfigSchemaFile(project); + }, 400); + + try { + // now we have the settings so we can re-build the logger + this._logger.level = this._settings?.debug === true ? 1 : 0; + // createServer() can be called with a custom config object, and + // this is a public interface that may be used by customized versions of the server + if (this._providedConfig) { + this._graphQLCache = new GraphQLCache({ + config: this._providedConfig, + logger: this._logger, + parser: this._parser, + configDir: rootDir, + onSchemaChange, + schemaCacheTTL: this._settings?.schemaCacheTTL, + }); + this._languageService = new GraphQLLanguageService( + this._graphQLCache, + this._logger, + ); + } else { + // reload the graphql cache + this._graphQLCache = await getGraphQLCache({ + parser: this._parser, + loadConfigOptions: this._loadConfigOptions, + logger: this._logger, + onSchemaChange, + schemaCacheTTL: this._settings?.schemaCacheTTL, + }); + this._languageService = new GraphQLLanguageService( + this._graphQLCache, + this._logger, + ); + } + const config = this._graphQLCache.getGraphQLConfig(); + if (config) { await this._cacheAllProjectFiles(config); + // TODO: per project lazy instantiation. + // we had it working before, but it seemed like it caused bugs + // which were caused by something else. + // thus. _isInitialized should be replaced with something like + // projectHasInitialized: (projectName: string) => boolean + this._isInitialized = true; + this._isGraphQLConfigMissing = false; + this._logger.info('GraphQL Language Server caches initialized'); } - this._isInitialized = true; } catch (err) { this._handleConfigError({ err }); } } - _handleConfigError({ err }: { err: unknown; uri?: string }) { - // console.log(err, typeof err); + private _handleConfigError({ err }: { err: unknown; uri?: string }) { if (err instanceof ConfigNotFoundError || err instanceof ConfigEmptyError) { // TODO: obviously this needs to become a map by workspace from uri // for workspaces support @@ -267,7 +302,7 @@ export class MessageProcessor { // this is the only case where we don't invalidate config; // TODO: per-project schema initialization status (PR is almost ready) this._logConfigError( - 'Project not found for this file - make sure that a schema is present', + 'Project not found for this file - make sure that a schema is present in the config file or for the project', ); } else if (err instanceof ConfigInvalidError) { this._isGraphQLConfigMissing = true; @@ -288,14 +323,14 @@ export class MessageProcessor { } } - _logConfigError(errorMessage: string) { + private _logConfigError(errorMessage: string) { this._logger.error( 'WARNING: graphql-config error, only highlighting is enabled:\n' + errorMessage + `\nfor more information on using 'graphql-config' with 'graphql-language-service-server', \nsee the documentation at ${configDocLink}`, ); } - async _isGraphQLConfigFile(uri: string) { + private async _isGraphQLConfigFile(uri: string) { const configMatchers = ['graphql.config', 'graphqlrc', 'graphqlconfig']; if (this._settings?.load?.fileName?.length) { configMatchers.push(this._settings.load.fileName); @@ -308,34 +343,54 @@ export class MessageProcessor { return fileMatch; } if (uri.match('package.json')?.length) { - const graphqlConfig = await import(URI.parse(uri).fsPath); - return Boolean(graphqlConfig?.graphql); + try { + const pkgConfig = await readFile(URI.parse(uri).fsPath, 'utf-8'); + return Boolean(JSON.parse(pkgConfig)?.graphql); + } catch {} } return false; } - - async handleDidOpenOrSaveNotification( - params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, - ): Promise { - /** - * Initialize the LSP server when the first file is opened or saved, - * so that we can access the user settings for config rootDir, etc - */ - const isGraphQLConfigFile = await this._isGraphQLConfigFile( - params.textDocument.uri, - ); + private async _loadConfigOrSkip(uri: string) { try { - if (!this._isInitialized || !this._graphQLCache) { - // don't try to initialize again if we've already tried - // and the graphql config file or package.json entry isn't even there + const isGraphQLConfigFile = await this._isGraphQLConfigFile(uri); + + if (!this._isInitialized) { if (this._isGraphQLConfigMissing === true && !isGraphQLConfigFile) { - return null; + return true; } - // then initial call to update graphql config - await this._updateGraphQLConfig(); + // don't try to initialize again if we've already tried + // and the graphql config file or package.json entry isn't even there + await this._initializeGraphQLCaches(); + return isGraphQLConfigFile; } + // if it has initialized, but this is another config file change, then let's handle it + if (isGraphQLConfigFile) { + await this._initializeGraphQLCaches(); + } + return isGraphQLConfigFile; } catch (err) { this._logger.error(String(err)); + // return true if it's a graphql config file so we don't treat + // this as a non-config file if it is one + return true; + } + } + + public async handleDidOpenOrSaveNotification( + params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, + ): Promise { + const { textDocument } = params; + const { uri } = textDocument; + + /** + * Initialize the LSP server when the first file is opened or saved, + * so that we can access the user settings for config rootDir, etc + */ + const shouldSkip = await this._loadConfigOrSkip(uri); + // if we're loading config or the config is missing or there's an error + // don't do anything else + if (shouldSkip) { + return { uri, diagnostics: [] }; } // Here, we set the workspace settings in memory, @@ -344,55 +399,46 @@ export class MessageProcessor { // We aren't able to use initialization event for this // and the config change event is after the fact. - if (!params?.textDocument) { + if (!textDocument) { throw new Error('`textDocument` argument is required.'); } - const { textDocument } = params; - const { uri } = textDocument; const diagnostics: Diagnostic[] = []; - let contents: CachedContent[] = []; - const text = 'text' in textDocument && textDocument.text; - // Create/modify the cached entry if text is provided. - // Otherwise, try searching the cache to perform diagnostics. - if (text) { - // textDocument/didSave does not pass in the text content. - // Only run the below function if text is passed in. - contents = this._parser(text, uri); - - await this._invalidateCache(textDocument, uri, contents); - } else { - if (isGraphQLConfigFile) { - this._logger.info('updating graphql config'); - await this._updateGraphQLConfig(); - return { uri, diagnostics: [] }; - } - return null; - } - if (!this._graphQLCache) { + if (!this._isInitialized) { return { uri, diagnostics }; } try { const project = this._graphQLCache.getProjectForFile(uri); - if ( - this._isInitialized && - project?.extensions?.languageService?.enableValidation !== false - ) { - await Promise.all( - contents.map(async ({ query, range }) => { - const results = await this._languageService.getDiagnostics( - query, - uri, - this._isRelayCompatMode(query), - ); - if (results && results.length > 0) { - diagnostics.push( - ...processDiagnosticsMessage(results, query, range), - ); - } - }), + + if (project) { + const text = 'text' in textDocument && textDocument.text; + // for some reason if i try to tell to not parse empty files, it breaks :shrug: + // i think this is because if the file change is empty, it doesn't get parsed + // TODO: this could be related to a bug in how we are calling didOpenOrSave in our tests + // that doesn't reflect the real runtime behavior + + const { contents } = await this._parseAndCacheFile( + uri, + project, + text as string, ); + if (project?.extensions?.languageService?.enableValidation !== false) { + await Promise.all( + contents.map(async ({ query, range }) => { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + diagnostics.push( + ...processDiagnosticsMessage(results, query, range), + ); + } + }), + ); + } } this._logger.log( @@ -403,14 +449,14 @@ export class MessageProcessor { fileName: uri, }), ); + return { uri, diagnostics }; } catch (err) { this._handleConfigError({ err, uri }); + return { uri, diagnostics }; } - - return { uri, diagnostics }; } - async handleDidChangeNotification( + public async handleDidChangeNotification( params: DidChangeTextDocumentParams, ): Promise { if ( @@ -431,46 +477,47 @@ export class MessageProcessor { } const { textDocument, contentChanges } = params; const { uri } = textDocument; - const project = this._graphQLCache.getProjectForFile(uri); + try { - const contentChange = contentChanges.at(-1)!; + const project = this._graphQLCache.getProjectForFile(uri); + if (!project) { + return { uri, diagnostics: [] }; + } // As `contentChanges` is an array, and we just want the // latest update to the text, grab the last entry from the array. // If it's a .js file, try parsing the contents to see if GraphQL queries // exist. If not found, delete from the cache. - const contents = this._parser(contentChange.text, uri); - // If it's a .graphql file, proceed normally and invalidate the cache. - await this._invalidateCache(textDocument, uri, contents); - - const cachedDocument = this._getCachedDocument(uri); - - if (!cachedDocument) { - return null; - } - - await this._updateFragmentDefinition(uri, contents); - await this._updateObjectTypeDefinition(uri, contents); + const { contents } = await this._parseAndCacheFile( + uri, + project, + contentChanges.at(-1)!.text, + ); + // // If it's a .graphql file, proceed normally and invalidate the cache. + // await this._invalidateCache(textDocument, uri, contents); const diagnostics: Diagnostic[] = []; if (project?.extensions?.languageService?.enableValidation !== false) { // Send the diagnostics onChange as well - await Promise.all( - contents.map(async ({ query, range }) => { - const results = await this._languageService.getDiagnostics( - query, - uri, - this._isRelayCompatMode(query), - ); - if (results && results.length > 0) { - diagnostics.push( - ...processDiagnosticsMessage(results, query, range), + try { + await Promise.all( + contents.map(async ({ query, range }) => { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), ); - } - }), - ); + if (results && results.length > 0) { + diagnostics.push( + ...processDiagnosticsMessage(results, query, range), + ); + } + // skip diagnostic errors, usually related to parsing incomplete fragments + }), + ); + } catch {} } this._logger.log( @@ -491,7 +538,7 @@ export class MessageProcessor { async handleDidChangeConfiguration( _params: DidChangeConfigurationParams, ): Promise { - await this._updateGraphQLConfig(); + await this._initializeGraphQLCaches(); this._logger.log( JSON.stringify({ type: 'usage', @@ -501,8 +548,8 @@ export class MessageProcessor { return {}; } - handleDidCloseNotification(params: DidCloseTextDocumentParams): void { - if (!this._isInitialized || !this._graphQLCache) { + public handleDidCloseNotification(params: DidCloseTextDocumentParams): void { + if (!this._isInitialized) { return; } // For every `textDocument/didClose` event, delete the cached entry. @@ -529,15 +576,15 @@ export class MessageProcessor { ); } - handleShutdownRequest(): void { + public handleShutdownRequest(): void { this._willShutdown = true; } - handleExitNotification(): void { + public handleExitNotification(): void { process.exit(this._willShutdown ? 0 : 1); } - validateDocumentAndPosition(params: CompletionParams): void { + private validateDocumentAndPosition(params: CompletionParams): void { if (!params?.textDocument?.uri || !params.position) { throw new Error( '`textDocument.uri` and `position` arguments are required.', @@ -545,11 +592,11 @@ export class MessageProcessor { } } - async handleCompletionRequest( + public async handleCompletionRequest( params: CompletionParams, - ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { - return []; + ): Promise { + if (!this._isInitialized) { + return { items: [], isIncomplete: false }; } this.validateDocumentAndPosition(params); @@ -563,7 +610,7 @@ export class MessageProcessor { const cachedDocument = this._getCachedDocument(textDocument.uri); if (!cachedDocument) { - return []; + return { items: [], isIncomplete: false }; } const found = cachedDocument.contents.find(content => { @@ -575,7 +622,7 @@ export class MessageProcessor { // If there is no GraphQL query in this file, return an empty result. if (!found) { - return []; + return { items: [], isIncomplete: false }; } const { query, range } = found; @@ -583,6 +630,7 @@ export class MessageProcessor { if (range) { position.line -= range.start.line; } + const result = await this._languageService.getAutocompleteSuggestions( query, toPosition(position), @@ -603,8 +651,10 @@ export class MessageProcessor { return { items: result, isIncomplete: false }; } - async handleHoverRequest(params: TextDocumentPositionParams): Promise { - if (!this._isInitialized || !this._graphQLCache) { + public async handleHoverRequest( + params: TextDocumentPositionParams, + ): Promise { + if (!this._isInitialized) { return { contents: [] }; } @@ -646,26 +696,34 @@ export class MessageProcessor { }; } - async handleWatchedFilesChangedNotification( - params: DidChangeWatchedFilesParams, - ): Promise | null> { - if ( - this._isGraphQLConfigMissing || - !this._isInitialized || - !this._graphQLCache - ) { - return null; + private async _parseAndCacheFile( + uri: string, + project: GraphQLProjectConfig, + text?: string, + ) { + try { + const fileText = text || (await readFile(URI.parse(uri).fsPath, 'utf-8')); + const contents = await this._parser(fileText, uri); + const cachedDocument = this._textDocumentCache.get(uri); + const version = cachedDocument ? cachedDocument.version++ : 0; + await this._invalidateCache({ uri, version }, uri, contents); + await this._updateFragmentDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents, project); + await this._updateSchemaIfChanged(project, uri); + return { contents, version }; + } catch { + return { contents: [], version: 0 }; } + } - return Promise.all( + public async handleWatchedFilesChangedNotification( + params: DidChangeWatchedFilesParams, + ): Promise | null> { + const resultsForChanges = Promise.all( params.changes.map(async (change: FileEvent) => { - if ( - this._isGraphQLConfigMissing || - !this._isInitialized || - !this._graphQLCache - ) { - this._logger.warn('No cache available for handleWatchedFilesChanged'); - return; + const shouldSkip = await this._loadConfigOrSkip(change.uri); + if (shouldSkip) { + return { uri: change.uri, diagnostics: [] }; } if ( change.type === FileChangeTypeKind.Created || @@ -673,75 +731,60 @@ export class MessageProcessor { ) { const { uri } = change; - const text = readFileSync(URI.parse(uri).fsPath, 'utf-8'); - const contents = this._parser(text, uri); - - await this._updateFragmentDefinition(uri, contents); - await this._updateObjectTypeDefinition(uri, contents); - try { + let diagnostics: Diagnostic[] = []; const project = this._graphQLCache.getProjectForFile(uri); if (project) { - await this._updateSchemaIfChanged(project, uri); + // Important! Use system file uri not file path here!!!! + const { contents } = await this._parseAndCacheFile(uri, project); + if ( + project?.extensions?.languageService?.enableValidation !== false + ) { + diagnostics = ( + await Promise.all( + contents.map(async ({ query, range }) => { + const results = + await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + return processDiagnosticsMessage(results, query, range); + } + return []; + }), + ) + ).reduce((left, right) => left.concat(right), diagnostics); + } + + return { uri, diagnostics }; } - - let diagnostics: Diagnostic[] = []; - - if ( - project?.extensions?.languageService?.enableValidation !== false - ) { - diagnostics = ( - await Promise.all( - contents.map(async ({ query, range }) => { - const results = await this._languageService.getDiagnostics( - query, - uri, - this._isRelayCompatMode(query), - ); - if (results && results.length > 0) { - return processDiagnosticsMessage(results, query, range); - } - return []; - }), - ) - ).reduce((left, right) => left.concat(right), diagnostics); - } - - this._logger.log( - JSON.stringify({ - type: 'usage', - messageType: 'workspace/didChangeWatchedFiles', - projectName: project?.name, - fileName: uri, - }), - ); - return { uri, diagnostics }; - } catch (err) { - this._handleConfigError({ err, uri }); - return { uri, diagnostics: [] }; - } + // skip diagnostics errors usually from incomplete files + } catch {} + return { uri, diagnostics: [] }; } if (change.type === FileChangeTypeKind.Deleted) { - await this._graphQLCache.updateFragmentDefinitionCache( - this._graphQLCache.getGraphQLConfig().dirpath, - change.uri, - false, - ); - await this._graphQLCache.updateObjectTypeDefinitionCache( - this._graphQLCache.getGraphQLConfig().dirpath, - change.uri, - false, - ); + await this._updateFragmentDefinition(change.uri, []); + await this._updateObjectTypeDefinition(change.uri, []); } }), ); + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'workspace/didChangeWatchedFiles', + files: params.changes.map(change => change.uri), + }), + ); + return resultsForChanges; } - async handleDefinitionRequest( + public async handleDefinitionRequest( params: TextDocumentPositionParams, _token?: CancellationToken, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } @@ -750,11 +793,8 @@ export class MessageProcessor { } const { textDocument, position } = params; const project = this._graphQLCache.getProjectForFile(textDocument.uri); - if (project) { - await this._cacheSchemaFilesForProject(project); - } const cachedDocument = this._getCachedDocument(textDocument.uri); - if (!cachedDocument) { + if (!cachedDocument || !project) { return []; } @@ -775,7 +815,7 @@ export class MessageProcessor { position.line -= parentRange.start.line; } - let result = null; + let result: DefinitionQueryResponse | null = null; try { result = await this._languageService.getDefinition( @@ -796,17 +836,26 @@ export class MessageProcessor { }); } catch {} + const locateCommand = project?.extensions?.languageService + ?.locateCommand as LocateCommand | undefined; + const formatted = result ? result.definitions.map(res => { const defRange = res.range as Range; - if (parentRange && res.name) { const isInline = inlineFragments.includes(res.name); const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes( - path.extname(textDocument.uri) as SupportedExtensionsEnum, + path.extname(res.path) as SupportedExtensionsEnum, ); - if (isInline && isEmbedded) { - const vOffset = parentRange.start.line; + + if (isEmbedded || isInline) { + const cachedDoc = this._getCachedDocument( + URI.parse(res.path).toString(), + ); + const vOffset = isEmbedded + ? cachedDoc?.contents[0].range?.start.line ?? 0 + : parentRange.start.line; + defRange.setStart( (defRange.start.line += vOffset), defRange.start.character, @@ -817,10 +866,22 @@ export class MessageProcessor { ); } } + + if (locateCommand && result && result?.printedName) { + const locateResult = this._getCustomLocateResult( + project, + result, + locateCommand, + ); + + if (locateResult) { + return locateResult; + } + } return { uri: res.path, range: defRange, - } as Location; + }; }) : []; @@ -834,11 +895,44 @@ export class MessageProcessor { ); return formatted; } + _getCustomLocateResult( + project: GraphQLProjectConfig, + result: DefinitionQueryResponse, + locateCommand: LocateCommand, + ) { + if (!result.printedName) { + return null; + } + try { + const locateResult = locateCommand(project.name, result.printedName, { + node: result.node, + type: result.type, + project, + }); + if (typeof locateResult === 'string') { + const [uri, startLine = '1', endLine = '1'] = locateResult.split(':'); + return { + uri, + range: new Range( + new Position(parseInt(startLine, 10), 0), + new Position(parseInt(endLine, 10), 0), + ), + }; + } + return locateResult; + } catch (error) { + this._logger.error( + 'There was an error executing user defined locateCommand\n\n' + + (error as Error).toString(), + ); + return null; + } + } - async handleDocumentSymbolRequest( + public async handleDocumentSymbolRequest( params: DocumentSymbolParams, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } @@ -895,14 +989,12 @@ export class MessageProcessor { // ); // } - async handleWorkspaceSymbolRequest( + public async handleWorkspaceSymbolRequest( params: WorkspaceSymbolParams, ): Promise> { - if (!this._isInitialized || !this._graphQLCache) { + if (!this._isInitialized) { return []; } - // const config = await this._graphQLCache.getGraphQLConfig(); - // await this._cacheAllProjectFiles(config); if (params.query !== '') { const documents = this._getTextDocuments(); @@ -910,6 +1002,7 @@ export class MessageProcessor { await Promise.all( documents.map(async ([uri]) => { const cachedDocument = this._getCachedDocument(uri); + if (!cachedDocument) { return []; } @@ -920,49 +1013,60 @@ export class MessageProcessor { symbols.push(...docSymbols); }), ); - return symbols.filter( - symbol => symbol?.name && symbol.name.includes(params.query), - ); + return symbols.filter(symbol => symbol?.name?.includes(params.query)); } return []; } - _getTextDocuments() { + private _getTextDocuments() { return Array.from(this._textDocumentCache); } - async _cacheSchemaText(uri: string, text: string, version: number) { + private async _cacheSchemaText( + uri: string, + text: string, + version: number, + project?: GraphQLProjectConfig, + ) { try { - const contents = this._parser(text, uri); + const contents = await this._parser(text, uri); if (contents.length > 0) { await this._invalidateCache({ version, uri }, uri, contents); - await this._updateObjectTypeDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents, project); } } catch (err) { this._logger.error(String(err)); } } - async _cacheSchemaFile( - _uri: UnnormalizedTypeDefPointer, + private async _cacheSchemaFile( + fileUri: UnnormalizedTypeDefPointer, project: GraphQLProjectConfig, ) { - const uri = _uri.toString(); - - const isFileUri = existsSync(uri); - let version = 1; - if (isFileUri) { - const schemaUri = URI.file(path.join(project.dirpath, uri)).toString(); - const schemaDocument = this._getCachedDocument(schemaUri); - - if (schemaDocument) { - version = schemaDocument.version++; + try { + // const parsedUri = URI.file(fileUri.toString()); + // @ts-expect-error + const matches = await glob(fileUri, { + cwd: project.dirpath, + absolute: true, + }); + const uri = matches[0]; + let version = 1; + if (uri) { + const schemaUri = URI.file(uri).toString(); + const schemaDocument = this._getCachedDocument(schemaUri); + + if (schemaDocument) { + version = schemaDocument.version++; + } + const schemaText = await readFile(uri, 'utf-8'); + await this._cacheSchemaText(schemaUri, schemaText, version); } - const schemaText = readFileSync(uri, 'utf8'); - await this._cacheSchemaText(schemaUri, schemaText, version); + } catch (err) { + this._logger.error(String(err)); } } - _getTmpProjectPath( + private _getTmpProjectPath( project: GraphQLProjectConfig, prependWithProtocol = true, appendPath?: string, @@ -972,7 +1076,9 @@ export class MessageProcessor { const basePath = path.join(this._tmpDirBase, workspaceName); let projectTmpPath = path.join(basePath, 'projects', project.name); if (!existsSync(projectTmpPath)) { - void mkdirp(projectTmpPath); + mkdirSync(projectTmpPath, { + recursive: true, + }); } if (appendPath) { projectTmpPath = path.join(projectTmpPath, appendPath); @@ -982,57 +1088,30 @@ export class MessageProcessor { } return path.resolve(projectTmpPath); } - /** - * Safely attempts to cache schema files based on a glob or path - * Exits without warning in several cases because these strings can be almost - * anything! - * @param uri - * @param project - */ - async _cacheSchemaPath(uri: string, project: GraphQLProjectConfig) { - try { - const files = await glob(uri); - if (files && files.length > 0) { - await Promise.all( - files.map(uriPath => this._cacheSchemaFile(uriPath, project)), - ); - } else { - try { - await this._cacheSchemaFile(uri, project); - } catch { - // this string may be an SDL string even, how do we even evaluate this? - } - } - } catch {} - } - async _cacheObjectSchema( - pointer: { [key: string]: any }, - project: GraphQLProjectConfig, - ) { - await Promise.all( - Object.keys(pointer).map(async schemaUri => - this._cacheSchemaPath(schemaUri, project), - ), - ); - } - async _cacheArraySchema( - pointers: UnnormalizedTypeDefPointer[], - project: GraphQLProjectConfig, - ) { - await Promise.all( - pointers.map(async schemaEntry => { - if (typeof schemaEntry === 'string') { - await this._cacheSchemaPath(schemaEntry, project); - } else if (schemaEntry) { - await this._cacheObjectSchema(schemaEntry, project); - } - }), - ); - } - async _cacheSchemaFilesForProject(project: GraphQLProjectConfig) { - const schema = project?.schema; + private getCachedSchemaSettings(project: GraphQLProjectConfig) { const config = project?.extensions?.languageService; + let cacheSchemaFileForLookup = true; + let schemaCacheTTL = 1000 * 30; + + if ( + config?.cacheSchemaFileForLookup === false || + this?._settings?.cacheSchemaFileForLookup === false + ) { + cacheSchemaFileForLookup = false; + } + // nullish coalescing allows 0 to be a valid value here + if (config?.schemaCacheTTL) { + schemaCacheTTL = config.schemaCacheTTL; + } + if (this?._settings?.schemaCacheTTL) { + schemaCacheTTL = this._settings.schemaCacheTTL; + } + return { cacheSchemaFileForLookup, schemaCacheTTL }; + } + + private async _cacheSchemaFilesForProject(project: GraphQLProjectConfig) { + // const config = project?.extensions?.languageService; /** * By default, we look for schema definitions in SDL files * @@ -1047,18 +1126,39 @@ export class MessageProcessor { * * it is disabled by default */ - const cacheSchemaFileForLookup = - config?.cacheSchemaFileForLookup ?? - this?._settings?.cacheSchemaFileForLookup ?? - false; - if (cacheSchemaFileForLookup) { - await this._cacheConfigSchema(project); - } else if (typeof schema === 'string') { - await this._cacheSchemaPath(schema, project); - } else if (Array.isArray(schema)) { - await this._cacheArraySchema(schema, project); - } else if (schema) { - await this._cacheObjectSchema(schema, project); + const { cacheSchemaFileForLookup } = this.getCachedSchemaSettings(project); + const unwrappedSchema = unwrapProjectSchema(project); + + // only local schema lookups if all of the schema entries are local files + const sdlOnly = isProjectSDLOnly(unwrappedSchema); + + // const uri = this._getTmpProjectPath( + // project, + // true, + // 'generated-schema.graphql', + // ); + // const fsPath = this._getTmpProjectPath( + // project, + // false, + // 'generated-schema.graphql', + // ); + // invalidate the cache for the generated schema file + // whether or not the user will continue using this feature + // because sdlOnly needs this file to be removed as well if the user is switching schemas + // this._textDocumentCache.delete(uri); + // skip exceptions if the file doesn't exist + try { + // await rm(fsPath, { force: true }); + } catch {} + // if we are caching the config schema, and it isn't a .graphql file, cache it + if (cacheSchemaFileForLookup && !sdlOnly) { + await this.cacheConfigSchemaFile(project); + } else if (sdlOnly) { + await Promise.all( + unwrappedSchema.map(async schemaEntry => + this._cacheSchemaFile(schemaEntry, project), + ), + ); } } /** @@ -1066,7 +1166,7 @@ export class MessageProcessor { * from GraphQLCache.getSchema() * @param project {GraphQLProjectConfig} */ - async _cacheConfigSchema(project: GraphQLProjectConfig) { + private async cacheConfigSchemaFile(project: GraphQLProjectConfig) { try { const schema = await this._graphQLCache.getSchema(project.name); if (schema) { @@ -1087,18 +1187,25 @@ export class MessageProcessor { schemaText = `# This is an automatically generated representation of your schema.\n# Any changes to this file will be overwritten and will not be\n# reflected in the resulting GraphQL schema\n\n${schemaText}`; const cachedSchemaDoc = this._getCachedDocument(uri); + this._graphQLCache._schemaMap.set(project.name, schema); + try { + await mkdir(path.dirname(fsPath), { recursive: true }); + } catch {} if (!cachedSchemaDoc) { - await writeFile(fsPath, schemaText, 'utf8'); - await this._cacheSchemaText(uri, schemaText, 1); + await writeFile(fsPath, schemaText, { + encoding: 'utf-8', + }); + await this._cacheSchemaText(uri, schemaText, 0, project); } // do we have a change in the getSchema result? if so, update schema cache if (cachedSchemaDoc) { - writeFileSync(fsPath, schemaText, 'utf8'); + await writeFile(fsPath, schemaText, 'utf-8'); await this._cacheSchemaText( uri, schemaText, cachedSchemaDoc.version++, + project, ); } } @@ -1113,7 +1220,7 @@ export class MessageProcessor { * * @param project {GraphQLProjectConfig} */ - async _cacheDocumentFilesforProject(project: GraphQLProjectConfig) { + private async _cacheDocumentFilesforProject(project: GraphQLProjectConfig) { try { const documents = await project.getDocuments(); return Promise.all( @@ -1131,7 +1238,7 @@ export class MessageProcessor { const uri = URI.file(filePath).toString(); // I would use the already existing graphql-config AST, but there are a few reasons we can't yet - const contents = this._parser(document.rawSDL, uri); + const contents = await this._parser(document.rawSDL, uri); if (!contents[0]?.query) { return; } @@ -1152,13 +1259,24 @@ export class MessageProcessor { * Caching all the document files upfront could be expensive. * @param config {GraphQLConfig} */ - async _cacheAllProjectFiles(config: GraphQLConfig) { + private async _cacheAllProjectFiles(config: GraphQLConfig) { if (config?.projects) { return Promise.all( Object.keys(config.projects).map(async projectName => { const project = config.getProject(projectName); + await this._cacheSchemaFilesForProject(project); - await this._cacheDocumentFilesforProject(project); + if (project.documents?.length) { + await this._cacheDocumentFilesforProject(project); + } else { + this._logger.warn( + [ + `No 'documents' config found for project: ${projectName}.`, + 'Fragments and query documents cannot be detected.', + 'LSP server will only perform some partial validation and SDL features.', + ].join('\n'), + ); + } }), ); } @@ -1169,61 +1287,61 @@ export class MessageProcessor { ); } - async _updateFragmentDefinition( + private async _updateFragmentDefinition( uri: Uri, contents: CachedContent[], ): Promise { - const rootDir = this._graphQLCache.getGraphQLConfig().dirpath; - - await this._graphQLCache.updateFragmentDefinition(rootDir, uri, contents); + const project = this._graphQLCache.getProjectForFile(uri); + if (project) { + const cacheKey = this._graphQLCache._cacheKeyForProject(project); + await this._graphQLCache.updateFragmentDefinition( + cacheKey, + uri, + contents, + ); + } } - async _updateSchemaIfChanged( + private async _updateSchemaIfChanged( project: GraphQLProjectConfig, uri: Uri, ): Promise { await Promise.all( - this._unwrapProjectSchema(project).map(async schema => { + unwrapProjectSchema(project).map(async schema => { const schemaFilePath = path.resolve(project.dirpath, schema); const uriFilePath = URI.parse(uri).fsPath; + if (uriFilePath === schemaFilePath) { - await this._graphQLCache.invalidateSchemaCacheForProject(project); + try { + const file = await readFile(schemaFilePath, 'utf-8'); + // only invalidate the schema cache if we can actually parse the file + // otherwise, leave the last valid one in place + parse(file, { noLocation: true }); + this._graphQLCache.invalidateSchemaCacheForProject(project); + } catch {} } }), ); } - _unwrapProjectSchema(project: GraphQLProjectConfig): string[] { - const projectSchema = project.schema; - - const schemas: string[] = []; - if (typeof projectSchema === 'string') { - schemas.push(projectSchema); - } else if (Array.isArray(projectSchema)) { - for (const schemaEntry of projectSchema) { - if (typeof schemaEntry === 'string') { - schemas.push(schemaEntry); - } else if (schemaEntry) { - schemas.push(...Object.keys(schemaEntry)); - } - } - } else { - schemas.push(...Object.keys(projectSchema)); - } - - return schemas; - } - - async _updateObjectTypeDefinition( + private async _updateObjectTypeDefinition( uri: Uri, contents: CachedContent[], + project?: GraphQLProjectConfig, ): Promise { - const rootDir = this._graphQLCache.getGraphQLConfig().dirpath; - - await this._graphQLCache.updateObjectTypeDefinition(rootDir, uri, contents); + const resolvedProject = + project ?? (await this._graphQLCache.getProjectForFile(uri)); + if (resolvedProject) { + const cacheKey = this._graphQLCache._cacheKeyForProject(resolvedProject); + await this._graphQLCache.updateObjectTypeDefinition( + cacheKey, + uri, + contents, + ); + } } - _getCachedDocument(uri: string): CachedDocumentType | null { + private _getCachedDocument(uri: string): CachedDocumentType | null { if (this._textDocumentCache.has(uri)) { const cachedDocument = this._textDocumentCache.get(uri); if (cachedDocument) { @@ -1233,7 +1351,7 @@ export class MessageProcessor { return null; } - async _invalidateCache( + private async _invalidateCache( textDocument: VersionedTextDocumentIdentifier, uri: Uri, contents: CachedContent[], @@ -1242,7 +1360,6 @@ export class MessageProcessor { const cachedDocument = this._textDocumentCache.get(uri); if ( cachedDocument && - textDocument && textDocument?.version && cachedDocument.version < textDocument.version ) { @@ -1261,7 +1378,7 @@ export class MessageProcessor { } } -function processDiagnosticsMessage( +export function processDiagnosticsMessage( results: Diagnostic[], query: string, range: RangeType | null, diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLCache.test.ts similarity index 99% rename from packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts rename to packages/graphql-language-service-server/src/__tests__/GraphQLCache.test.ts index 54082249e3f..10c87d5e367 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLCache.test.ts @@ -177,7 +177,7 @@ describe('GraphQLCache', () => { ' `,\n' + ' },\n' + '});'; - const contents = parseDocument(text, 'test.js'); + const contents = await parseDocument(text, 'test.js'); const result = await cache.getFragmentDependenciesForAST( parse(contents[0].query), fragmentDefinitions, diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService.test.ts similarity index 77% rename from packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts rename to packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService.test.ts index 0283c277174..62e14a12a82 100644 --- a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService.test.ts @@ -14,6 +14,7 @@ import { GraphQLLanguageService } from '../GraphQLLanguageService'; import { SymbolKind } from 'vscode-languageserver-protocol'; import { Position } from 'graphql-language-service'; import { NoopLogger } from '../Logger'; +import { GraphQLEnumType } from 'graphql'; const MOCK_CONFIG = { filepath: join(__dirname, '.graphqlrc.yml'), @@ -27,7 +28,7 @@ describe('GraphQLLanguageService', () => { const mockCache = { async getSchema() { const config = this.getGraphQLConfig(); - return config.getDefault()!.getSchema(); + return config.getDefault().getSchema(); }, getGraphQLConfig() { @@ -37,7 +38,29 @@ describe('GraphQLLanguageService', () => { getProjectForFile(uri: string) { return this.getGraphQLConfig().getProjectForFile(uri); }, - + getFragmentDefinitions() { + const definitions = new Map(); + definitions.set('TestFragment', { + filePath: 'fake file path', + content: 'fake file content', + definition: { + kind: 'FragmentDefinition', + name: { + value: 'TestFragment', + }, + loc: { + start: 293, + end: 335, + }, + }, + }); + return definitions; + }, + // setting the defs here in duplicate as with object types below + // leads to duplicates, perhaps related to a bug, or perhaps just a test bug? + getFragmentDependenciesForAST() { + return []; + }, getObjectTypeDefinitions() { const definitions = new Map(); @@ -71,6 +94,16 @@ describe('GraphQLLanguageService', () => { start: 293, end: 335, }, + arguments: [ + { + name: { value: 'arg' }, + loc: { + start: 293, + end: 335, + }, + type: GraphQLEnumType, + }, + ], }, ], @@ -166,6 +199,23 @@ describe('GraphQLLanguageService', () => { expect(definitionQueryResult?.definitions.length).toEqual(1); }); + it('runs definition service on fragment definition', async () => { + const definitionQueryResult = await languageService.getDefinition( + 'fragment TestFragment on Human { name }', + { line: 0, character: 14 } as Position, + './queries/definitionQuery.graphql', + ); + expect(definitionQueryResult?.definitions.length).toEqual(1); + }); + it('runs definition service on fragment spread', async () => { + const definitionQueryResult = await languageService.getDefinition( + 'fragment TestFragment on Human { name }\nquery { ...TestFragment }', + { line: 1, character: 14 } as Position, + './queries/definitionQuery.graphql', + ); + expect(definitionQueryResult?.definitions.length).toEqual(1); + }); + it('runs definition service on field as expected', async () => { const definitionQueryResult = await languageService.getDefinition( 'query XXX { human { name } }', diff --git a/packages/graphql-language-service-server/src/__tests__/Logger.test.ts b/packages/graphql-language-service-server/src/__tests__/Logger.test.ts new file mode 100644 index 00000000000..82ac05fd097 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/Logger.test.ts @@ -0,0 +1,39 @@ +import { Logger } from '../Logger'; + +describe('Logger', () => { + const connection = { + console: { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), + }, + onDidChangeConfiguration: jest.fn(), + workspace: { + getConfiguration: jest.fn(), + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize with default log level, and ignore .log intentionally', () => { + const logger = new Logger(connection as any); + expect(logger).toBeDefined(); + expect(logger.logLevel).toBe(0); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(0); + }); + + it('should not change log level when settings are not passed', () => { + const logger = new Logger(connection as any, true); + expect(logger).toBeDefined(); + expect(logger.logLevel).toBe(1); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(1); + expect(logger.logLevel).toBe(1); + logger.log('test'); + expect(connection.console.log).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts deleted file mode 100644 index e2c2ecdaaf9..00000000000 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts +++ /dev/null @@ -1,601 +0,0 @@ -/** - * Copyright (c) 2021 GraphQL Contributors - * All rights reserved. - * - * This source code is licensed under the license found in the - * LICENSE file in the root directory of this source tree. - * - */ -import { SymbolKind } from 'vscode-languageserver'; -import { FileChangeType } from 'vscode-languageserver-protocol'; -import { Position, Range } from 'graphql-language-service'; - -import { MessageProcessor } from '../MessageProcessor'; -import { parseDocument } from '../parseDocument'; - -jest.mock('../Logger'); - -import { GraphQLCache } from '../GraphQLCache'; - -import { loadConfig } from 'graphql-config'; - -import type { DefinitionQueryResult, Outline } from 'graphql-language-service'; - -import { NoopLogger } from '../Logger'; -import { pathToFileURL } from 'node:url'; -import mockfs from 'mock-fs'; -import { join } from 'node:path'; - -jest.mock('node:fs', () => ({ - ...jest.requireActual('fs'), - readFileSync: jest.fn(jest.requireActual('fs').readFileSync), -})); - -describe('MessageProcessor', () => { - const logger = new NoopLogger(); - const messageProcessor = new MessageProcessor({ - // @ts-ignore - connection: {}, - logger, - graphqlFileExtensions: ['graphql'], - loadConfigOptions: { rootDir: __dirname }, - }); - - const queryPathUri = pathToFileURL(`${__dirname}/__queries__`); - const textDocumentTestString = ` - { - hero(episode: NEWHOPE){ - } - } - `; - - beforeEach(async () => { - const gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); - // loadConfig.mockRestore(); - messageProcessor._settings = { load: {} }; - messageProcessor._graphQLCache = new GraphQLCache({ - configDir: __dirname, - config: gqlConfig, - parser: parseDocument, - logger: new NoopLogger(), - }); - messageProcessor._languageService = { - // @ts-ignore - getAutocompleteSuggestions(query, position, uri) { - return [{ label: `${query} at ${uri}` }]; - }, - // @ts-ignore - getDiagnostics(_query, _uri) { - return []; - }, - async getDocumentSymbols(_query: string, uri: string) { - return [ - { - name: 'item', - kind: SymbolKind.Field, - location: { - uri, - range: { - start: { line: 1, character: 2 }, - end: { line: 1, character: 4 }, - }, - }, - }, - ]; - }, - async getOutline(_query: string): Promise { - return { - outlineTrees: [ - { - representativeName: 'item', - kind: 'Field', - startPosition: new Position(1, 2), - endPosition: new Position(1, 4), - children: [], - }, - ], - }; - }, - async getDefinition( - _query, - position, - uri, - ): Promise { - return { - queryRange: [new Range(position, position)], - definitions: [ - { - position, - path: uri, - }, - ], - }; - }, - }; - }); - - let getConfigurationReturnValue = {}; - // @ts-ignore - messageProcessor._connection = { - // @ts-ignore - get workspace() { - return { - async getConfiguration() { - return [getConfigurationReturnValue]; - }, - }; - }, - }; - - const initialDocument = { - textDocument: { - text: textDocumentTestString, - uri: `${queryPathUri}/test.graphql`, - version: 0, - }, - }; - - messageProcessor._isInitialized = true; - - it('initializes properly and opens a file', async () => { - const { capabilities } = await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: __dirname, - }, - null, - __dirname, - ); - expect(capabilities.definitionProvider).toEqual(true); - expect(capabilities.workspaceSymbolProvider).toEqual(true); - expect(capabilities.completionProvider.resolveProvider).toEqual(true); - expect(capabilities.textDocumentSync).toEqual(1); - }); - - it('runs completion requests properly', async () => { - const uri = `${queryPathUri}/test2.graphql`; - const query = 'test'; - messageProcessor._textDocumentCache.set(uri, { - version: 0, - contents: [ - { - query, - range: new Range(new Position(0, 0), new Position(0, 0)), - }, - ], - }); - - const test = { - position: new Position(0, 0), - textDocument: { uri }, - }; - const result = await messageProcessor.handleCompletionRequest(test); - expect(result).toEqual({ - items: [{ label: `${query} at ${uri}` }], - isIncomplete: false, - }); - }); - - it('runs document symbol requests', async () => { - const uri = `${queryPathUri}/test3.graphql`; - const validQuery = ` - { - hero(episode: EMPIRE){ - ...testFragment - } - } - `; - - const newDocument = { - textDocument: { - text: validQuery, - uri, - version: 0, - }, - }; - - messageProcessor._textDocumentCache.set(uri, { - version: 0, - contents: [ - { - query: validQuery, - range: new Range(new Position(0, 0), new Position(0, 0)), - }, - ], - }); - - const test = { - textDocument: newDocument.textDocument, - }; - - const result = await messageProcessor.handleDocumentSymbolRequest(test); - - expect(result).not.toBeUndefined(); - expect(result.length).toEqual(1); - expect(result[0].name).toEqual('item'); - expect(result[0].kind).toEqual(SymbolKind.Field); - expect(result[0].location.range).toEqual({ - start: { line: 1, character: 2 }, - end: { line: 1, character: 4 }, - }); - }); - - it('properly changes the file cache with the didChange handler', async () => { - const uri = `${queryPathUri}/test.graphql`; - messageProcessor._textDocumentCache.set(uri, { - version: 1, - contents: [ - { - query: '', - range: new Range(new Position(0, 0), new Position(0, 0)), - }, - ], - }); - const textDocumentChangedString = ` - { - hero(episode: NEWHOPE){ - name - } - } - `; - - const result = await messageProcessor.handleDidChangeNotification({ - textDocument: { - // @ts-ignore - text: textDocumentTestString, - uri, - version: 1, - }, - contentChanges: [ - { text: textDocumentTestString }, - { text: textDocumentChangedString }, - ], - }); - // Query fixed, no more errors - expect(result.diagnostics.length).toEqual(0); - }); - - it('does not crash on null value returned in response to workspace configuration', async () => { - const previousConfigurationValue = getConfigurationReturnValue; - getConfigurationReturnValue = null; - await expect( - messageProcessor.handleDidChangeConfiguration(), - ).resolves.toStrictEqual({}); - getConfigurationReturnValue = previousConfigurationValue; - }); - - it('properly removes from the file cache with the didClose handler', async () => { - await messageProcessor.handleDidCloseNotification(initialDocument); - - const position = { line: 4, character: 5 }; - const params = { textDocument: initialDocument.textDocument, position }; - - // Should throw because file has been deleted from cache - try { - const result = await messageProcessor.handleCompletionRequest(params); - expect(result).toEqual(null); - } catch {} - }); - - // modified to work with jest.mock() of WatchmanClient - it('runs definition requests', async () => { - jest.setTimeout(10000); - const validQuery = ` - { - hero(episode: EMPIRE){ - ...testFragment - } - } - `; - - const newDocument = { - textDocument: { - text: validQuery, - uri: `${queryPathUri}/test3.graphql`, - version: 1, - }, - }; - messageProcessor._getCachedDocument = (_uri: string) => ({ - version: 1, - contents: [ - { - query: validQuery, - range: new Range(new Position(0, 0), new Position(20, 4)), - }, - ], - }); - - await messageProcessor.handleDidOpenOrSaveNotification(newDocument); - - const test = { - position: new Position(3, 15), - textDocument: newDocument.textDocument, - }; - - const result = await messageProcessor.handleDefinitionRequest(test); - await expect(result[0].uri).toEqual(`${queryPathUri}/test3.graphql`); - }); - - describe('handleDidOpenOrSaveNotification', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._updateGraphQLConfig = jest.fn(); - }); - it('updates config for standard config filename changes', async () => { - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/.graphql.config.js`, - languageId: 'js', - version: 0, - text: '', - }, - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - }); - - it('updates config for custom config filename changes', async () => { - const customConfigName = 'custom-config-name.yml'; - messageProcessor._settings = { load: { fileName: customConfigName } }; - - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/${customConfigName}`, - languageId: 'js', - version: 0, - text: '', - }, - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - }); - - it('handles config requests with no config', async () => { - messageProcessor._settings = {}; - - await messageProcessor.handleDidChangeConfiguration({ - settings: [], - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/.graphql.config.js`, - languageId: 'js', - version: 0, - text: '', - }, - }); - - expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); - }); - }); - - describe('handleWatchedFilesChangedNotification', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._updateGraphQLConfig = jest.fn(); - }); - - it('skips config updates for normal file changes', async () => { - await messageProcessor.handleWatchedFilesChangedNotification({ - changes: [ - { - uri: `${pathToFileURL('.')}/foo.graphql`, - type: FileChangeType.Changed, - }, - ], - }); - - expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); - }); - }); - - describe('handleWatchedFilesChangedNotification without graphql config', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._graphQLConfig = undefined; - messageProcessor._isGraphQLConfigMissing = true; - messageProcessor._parser = jest.fn(); - }); - - it('skips config updates for normal file changes', async () => { - await messageProcessor.handleWatchedFilesChangedNotification({ - changes: [ - { - uri: `${pathToFileURL('.')}/foo.js`, - type: FileChangeType.Changed, - }, - ], - }); - expect(messageProcessor._parser).not.toHaveBeenCalled(); - }); - }); - - describe('handleDidChangedNotification without graphql config', () => { - const mockReadFileSync: jest.Mock = - jest.requireMock('node:fs').readFileSync; - - beforeEach(() => { - mockReadFileSync.mockReturnValue(''); - messageProcessor._graphQLConfig = undefined; - messageProcessor._isGraphQLConfigMissing = true; - messageProcessor._parser = jest.fn(); - }); - - it('skips config updates for normal file changes', async () => { - await messageProcessor.handleDidChangeNotification({ - textDocument: { - uri: `${pathToFileURL('.')}/foo.js`, - version: 1, - }, - contentChanges: [{ text: 'var something' }], - }); - expect(messageProcessor._parser).not.toHaveBeenCalled(); - }); - }); -}); - -describe('MessageProcessor with no config', () => { - let messageProcessor: MessageProcessor; - const mockRoot = join('/tmp', 'test'); - let loggerSpy: jest.SpyInstance; - - const mockProcessor = (query: string, config?: string) => { - const items = { - 'query.graphql': query, - 'node_modules/parse-json': mockfs.load('node_modules/parse-json'), - }; - if (config) { - items['graphql.config.js'] = config; - } - const files: Record> = { - [mockRoot]: mockfs.directory({ - items, - }), - // node_modules: mockfs.load('node_modules'), - }; - mockfs(files); - const logger = new NoopLogger(); - loggerSpy = jest.spyOn(logger, 'error'); - messageProcessor = new MessageProcessor({ - // @ts-ignore - connection: { - // @ts-ignore - get workspace() { - return { - async getConfiguration() { - return []; - }, - }; - }, - }, - logger, - graphqlFileExtensions: ['graphql'], - loadConfigOptions: { rootDir: mockRoot }, - }); - }; - - beforeEach(() => {}); - - afterEach(() => { - mockfs.restore(); - }); - it('fails to initialize with empty config file', async () => { - mockProcessor('query { foo }', ''); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringMatching( - /GraphQL Config file is not available in the provided config directory/, - ), - ); - }); - it('fails to initialize with no config file present', async () => { - mockProcessor('query { foo }'); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(messageProcessor._isGraphQLConfigMissing).toEqual(true); - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringMatching( - /GraphQL Config file is not available in the provided config directory/, - ), - ); - }); - it('initializes when presented with a valid config later', async () => { - mockProcessor('query { foo }'); - await messageProcessor.handleInitializeRequest( - // @ts-ignore - { - rootPath: mockRoot, - }, - null, - mockRoot, - ); - await messageProcessor.handleDidOpenOrSaveNotification({ - textDocument: { - text: 'query { foo }', - uri: `${mockRoot}/query.graphql`, - version: 1, - }, - }); - expect(messageProcessor._isInitialized).toEqual(false); - expect(loggerSpy).toHaveBeenCalledTimes(1); - // todo: get mockfs working with in-test file changes - // mockfs.restore(); - // mockfs({ - // [mockRoot]: mockfs.directory({ - // mode: 0o755, - // items: { - // 'schema.graphql': - // 'type Query { foo: String }\nschema { query: Query }', - // 'graphql.config.js': mockfs.file({ - // content: 'module.exports = { schema: "schema.graphql" };', - // mode: 0o644, - // }), - // 'query.graphql': 'query { foo }', - // // 'node_modules/graphql-config/node_modules': mockfs.load( - // // 'node_modules/graphql-config/node_modules', - // // ), - // }, - // }), - // }); - // // console.log(readdirSync(`${mockRoot}`)); - // await messageProcessor.handleDidOpenOrSaveNotification({ - // textDocument: { - // text: 'module.exports = { schema: `schema.graphql` }', - // uri: `${mockRoot}/graphql.config.js`, - // version: 2, - // }, - // }); - - // expect(messageProcessor._isGraphQLConfigMissing).toEqual(false); - - // expect(loggerSpy).toHaveBeenCalledWith( - // expect.stringMatching( - // /GraphQL Config file is not available in the provided config directory/, - // ), - // ); - }); -}); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts new file mode 100644 index 00000000000..882e577e127 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -0,0 +1,640 @@ +import mockfs from 'mock-fs'; +import { join } from 'node:path'; +import { MockFile, MockProject } from './__utils__/MockProject'; +// import { readFileSync } from 'node:fs'; +import { FileChangeType } from 'vscode-languageserver'; +import { serializeRange } from './__utils__/utils'; +import { readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { URI } from 'vscode-uri'; +import { + GraphQLSchema, + buildASTSchema, + introspectionFromSchema, + parse, + version, +} from 'graphql'; +import fetchMock from 'fetch-mock'; + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +jest.mock('@whatwg-node/fetch', () => { + const { AbortController } = require('node-abort-controller'); + + return { + fetch: require('fetch-mock').fetchHandler, + AbortController, + TextDecoder: global.TextDecoder, + }; +}); + +const mockSchema = (schema: GraphQLSchema) => { + const introspectionResult = { + data: introspectionFromSchema(schema, { + descriptions: true, + }), + }; + return fetchMock.mock({ + matcher: '*', + response: { + headers: { + 'Content-Type': 'application/json', + }, + body: introspectionResult, + }, + }); +}; + +const defaultFiles = [ + ['query.graphql', 'query { bar ...B }'], + ['fragments.graphql', 'fragment B on Foo { bar }'], +] as MockFile[]; +const schemaFile: MockFile = [ + 'schema.graphql', + 'type Query { foo: Foo, test: Test }\n\ntype Foo { bar: String }\n\ntype Test { test: Foo }', +]; + +const fooTypePosition = { + start: { line: 2, character: 0 }, + end: { line: 2, character: 24 }, +}; + +const fooInlineTypePosition = { + start: { line: 5, character: 0 }, + end: { line: 5, character: 24 }, +}; + +const genSchemaPath = + '/tmp/graphql-language-service/test/projects/default/generated-schema.graphql'; + +// TODO: +// - reorganize into multiple files +// - potentially a high level abstraction and/or it.each() for a pathway across configs, file extensions, etc. +// this may be cumbersome with offset position assertions but possible +// if we can create consistency that doesn't limit variability +// - convert each it() into a nested describe() block (or a top level describe() in another file), and sprinkle in it() statements to replace comments +// - fix TODO comments where bugs were found that couldn't be resolved quickly (2-4hr time box) + +describe('MessageProcessor with no config', () => { + afterEach(() => { + mockfs.restore(); + fetchMock.restore(); + }); + it('fails to initialize with empty config file', async () => { + const project = new MockProject({ + files: [...defaultFiles, ['graphql.config.json', '']], + }); + await project.init(); + + expect(project.lsp._logger.info).toHaveBeenCalledTimes(1); + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); + expect(project.lsp._logger.error).toHaveBeenCalledWith( + expect.stringMatching( + /GraphQL Config file is not available in the provided config directory/, + ), + ); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + project.lsp.handleShutdownRequest(); + }); + it('fails to initialize with no config file present', async () => { + const project = new MockProject({ + files: [...defaultFiles], + }); + await project.init(); + + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); + expect(project.lsp._logger.error).toHaveBeenCalledWith( + expect.stringMatching( + /GraphQL Config file is not available in the provided config directory/, + ), + ); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + project.lsp.handleShutdownRequest(); + }); + it('initializes when presented with a valid config later', async () => { + const project = new MockProject({ + files: [...defaultFiles], + }); + await project.init(); + expect(project.lsp._isInitialized).toEqual(false); + expect(project.lsp._isGraphQLConfigMissing).toEqual(true); + expect(project.lsp._logger.error).toHaveBeenCalledTimes(1); + + project.changeFile( + 'graphql.config.json', + '{ "schema": "./schema.graphql" }', + ); + // TODO: this should work for on watched file changes as well! + await project.lsp.handleDidOpenOrSaveNotification({ + textDocument: { + uri: project.uri('graphql.config.json'), + }, + }); + expect(project.lsp._isInitialized).toEqual(true); + expect(project.lsp._isGraphQLConfigMissing).toEqual(false); + expect(project.lsp._graphQLCache).toBeDefined(); + project.lsp.handleShutdownRequest(); + }); +}); + +describe('MessageProcessor with config', () => { + afterEach(() => { + mockfs.restore(); + fetchMock.restore(); + }); + // beforeAll(async () => { + // app = await import('../../../graphiql/test/e2e-server'); + // }); + // afterAll(() => { + // app.server.close(); + // app.wsServer.close(); + // }); + it('caches files and schema with .graphql file config, and the schema updates with watched file changes', async () => { + const project = new MockProject({ + files: [ + schemaFile, + [ + 'graphql.config.json', + '{ "schema": "./schema.graphql", "documents": "./**.graphql" }', + ], + ...defaultFiles, + ], + }); + const results = await project.init('query.graphql'); + expect(results.diagnostics[0].message).toEqual( + 'Cannot query field "bar" on type "Query".', + ); + expect(results.diagnostics[1].message).toEqual( + 'Fragment "B" cannot be spread here as objects of type "Query" can never be of type "Foo".', + ); + const initSchemaDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('schema.graphql') }, + position: { character: 19, line: 0 }, + }); + expect(initSchemaDefRequest.length).toEqual(1); + expect(initSchemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(initSchemaDefRequest[0].range)).toEqual( + fooTypePosition, + ); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); + // TODO: for some reason the cache result formats the graphql query?? + const docCache = project.lsp._textDocumentCache; + expect( + docCache.get(project.uri('query.graphql'))!.contents[0].query, + ).toContain('...B'); + const schemaDefinitions = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(schemaDefinitions[0].uri).toEqual(project.uri('schema.graphql')); + + expect(serializeRange(schemaDefinitions[0].range)).toEqual(fooTypePosition); + + // query definition request of fragment name jumps to the fragment definition + const firstQueryDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, + position: { character: 16, line: 0 }, + }); + + expect(firstQueryDefRequest[0].uri).toEqual( + project.uri('fragments.graphql'), + ); + expect(serializeRange(firstQueryDefRequest[0].range)).toEqual({ + start: { + line: 0, + character: 0, + }, + end: { + line: 2, + character: 1, + }, + }); + + // change the file to make the fragment invalid + project.changeFile( + 'schema.graphql', + // now Foo has a bad field, the fragment should be invalid + 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\ntype Foo { bad: Int }', + ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('schema.graphql'), type: FileChangeType.Changed }, + ], + }); + const typeCache = + project.lsp._graphQLCache._typeDefinitionsCache.get('/tmp/test-default'); + expect(typeCache?.get('Test')?.definition.name.value).toEqual('Test'); + + // test in-file schema defs! important! + const schemaDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('schema.graphql') }, + position: { character: 19, line: 0 }, + }); + + const fooLaterTypePosition = { + start: { line: 7, character: 0 }, + end: { line: 7, character: 21 }, + }; + expect(schemaDefRequest.length).toEqual(1); + expect(schemaDefRequest[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(schemaDefRequest[0].range)).toEqual( + fooLaterTypePosition, + ); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + // change the file to make the fragment invalid + project.changeFile( + 'schema.graphql', + // now Foo has a bad field, the fragment should be invalid + 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', + ); + // await project.lsp.handleWatchedFilesChangedNotification({ + // changes: [ + // { + // type: FileChangeType.Changed, + // uri: project.uri('schema.graphql'), + // }, + // ], + // }); + await project.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeType.Changed, + text: 'type Query { foo: Foo, test: Test }\n\n type Test { test: String }\n\n\n\n\n\ntype Foo { bad: Int }', + }, + ], + textDocument: { uri: project.uri('schema.graphql'), version: 1 }, + }); + + const schemaDefRequest2 = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('schema.graphql') }, + position: { character: 19, line: 0 }, + }); + + const fooLaterTypePosition2 = { + start: { line: 8, character: 0 }, + end: { line: 8, character: 21 }, + }; + expect(schemaDefRequest2.length).toEqual(1); + expect(schemaDefRequest2[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(schemaDefRequest2[0].range)).toEqual( + fooLaterTypePosition2, + ); + + // TODO: this fragment should now be invalid + const result = await project.lsp.handleDidOpenOrSaveNotification({ + textDocument: { uri: project.uri('fragments.graphql') }, + }); + expect(result.diagnostics[0].message).toEqual( + 'Cannot query field "bar" on type "Foo". Did you mean "bad"?', + ); + const generatedFile = existsSync(join(genSchemaPath)); + // this generated file should not exist because the schema is local! + expect(generatedFile).toEqual(false); + // simulating codegen + project.changeFile( + 'fragments.graphql', + 'fragment A on Foo { bad }\n\nfragment B on Test { test }', + ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('fragments.graphql'), type: FileChangeType.Changed }, + ], + }); + + // TODO: this interface should maybe not be tested here but in unit tests + const fragCache = + project.lsp._graphQLCache._fragmentDefinitionsCache.get( + '/tmp/test-default', + ); + expect(fragCache?.get('A')?.definition.name.value).toEqual('A'); + expect(fragCache?.get('B')?.definition.name.value).toEqual('B'); + const queryFieldDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 22, line: 0 }, + }); + expect(queryFieldDefRequest[0].uri).toEqual(project.uri('schema.graphql')); + expect(serializeRange(queryFieldDefRequest[0].range)).toEqual({ + start: { + line: 8, + character: 11, + }, + end: { + line: 8, + character: 19, + }, + }); + + // on the second request, the position has changed + const secondQueryDefRequest = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(secondQueryDefRequest[0].uri).toEqual( + project.uri('fragments.graphql'), + ); + expect(serializeRange(secondQueryDefRequest[0].range)).toEqual({ + start: { + line: 2, + character: 0, + }, + end: { + line: 2, + character: 27, + }, + }); + // definitions request for fragments jumps to a different place in schema.graphql now + const schemaDefinitionsAgain = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 16, line: 0 }, + }); + expect(schemaDefinitionsAgain[0].uri).toEqual( + project.uri('schema.graphql'), + ); + + expect(serializeRange(schemaDefinitionsAgain[0].range)).toEqual( + fooLaterTypePosition2, + ); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + project.lsp.handleShutdownRequest(); + }); + + it('caches files and schema with a URL config', async () => { + const offset = parseInt(version, 10) > 16 ? 25 : 0; + + mockSchema(require('../../../graphiql/test/schema')); + + const project = new MockProject({ + files: [ + ['query.graphql', 'query { test { isTest, ...T } }'], + ['fragments.graphql', 'fragment T on Test {\n isTest \n}'], + [ + 'graphql.config.json', + '{ "schema": "http://localhost:3100/graphql", "documents": "./**" }', + ], + ], + }); + + const initParams = await project.init('query.graphql'); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + + expect(initParams.diagnostics).toEqual([]); + + const changeParams = await project.lsp.handleDidChangeNotification({ + textDocument: { uri: project.uri('query.graphql'), version: 1 }, + contentChanges: [{ text: 'query { test { isTest, ...T or } }' }], + }); + expect(changeParams?.diagnostics[0].message).toEqual( + 'Cannot query field "or" on type "Test".', + ); + expect(await project.lsp._graphQLCache.getSchema('default')).toBeDefined(); + + // schema file is present and contains schema + const file = await readFile(join(genSchemaPath), { encoding: 'utf-8' }); + expect(file.split('\n').length).toBeGreaterThan(10); + + // hover works + const hover = await project.lsp.handleHoverRequest({ + position: { + character: 10, + line: 0, + }, + textDocument: { uri: project.uri('query.graphql') }, + }); + expect(hover.contents).toContain('`test` field from `Test` type.'); + + // ensure that fragment definitions work + const definitions = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, // console.log(project.uri('query.graphql')) + position: { character: 26, line: 0 }, + }); + expect(definitions[0].uri).toEqual(project.uri('fragments.graphql')); + expect(serializeRange(definitions[0].range)).toEqual({ + start: { + line: 0, + character: 0, + }, + end: { + line: 2, + character: 1, + }, + }); + + const typeDefinitions = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.graphql') }, + position: { character: 15, line: 0 }, + }); + + expect(typeDefinitions[0].uri).toEqual(URI.parse(genSchemaPath).toString()); + + expect(serializeRange(typeDefinitions[0].range)).toEqual({ + start: { + line: 11 + offset, + character: 0, + }, + end: { + line: 102 + offset, + character: 1, + }, + }); + + const schemaDefs = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: URI.parse(genSchemaPath).toString() }, + position: { character: 20, line: 18 + offset }, + }); + expect(schemaDefs[0].uri).toEqual(URI.parse(genSchemaPath).toString()); + // note: if the graphiql test schema changes, + // this might break, please adjust if you see a failure here + expect(serializeRange(schemaDefs[0].range)).toEqual({ + start: { + line: 104 + offset, + character: 0, + }, + end: { + line: 112 + offset, + character: 1, + }, + }); + // lets remove the fragments file + await project.deleteFile('fragments.graphql'); + // and add a fragments.ts file, watched + await project.addFile( + 'fragments.ts', + '\n\n\nexport const fragment = gql`\n\n fragment T on Test { isTest } \n query { hasArgs(string: "") }\n`', + true, + ); + + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('fragments.ts'), type: FileChangeType.Created }, + ], + }); + const defsForTs = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('query.graphql') }, + position: { character: 26, line: 0 }, + }); + // this one is really important + expect(defsForTs[0].uri).toEqual(project.uri('fragments.ts')); + expect(serializeRange(defsForTs[0].range)).toEqual({ + start: { + line: 5, + character: 2, + }, + end: { + line: 5, + character: 31, + }, + }); + const defsForArgs = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('fragments.ts') }, + position: { character: 19, line: 6 }, + }); + + expect(defsForArgs[0].uri).toEqual(URI.parse(genSchemaPath).toString()); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + project.lsp.handleShutdownRequest(); + }); + + it('caches multiple projects with files and schema with a URL config and a local schema', async () => { + mockSchema(require('../../../graphiql/test/schema')); + + const project = new MockProject({ + files: [ + [ + 'a/fragments.ts', + '\n\n\nexport const fragment = gql`\n\n fragment TestFragment on Test { isTest }\n`', + ], + [ + 'a/query.ts', + '\n\n\nexport const query = graphql`query { test { isTest ...T } }`', + ], + + [ + 'b/query.ts', + 'import graphql from "graphql"\n\n\nconst a = graphql` query example { test() { isTest ...T } }`', + ], + [ + 'b/fragments.ts', + '\n\n\nexport const fragment = gql`\n\n fragment T on Test { isTest }\n`', + ], + [ + 'b/schema.ts', + `\n\nexport const schema = gql(\`\n${schemaFile[1]}\`)`, + ], + [ + 'package.json', + `{ "graphql": { "projects": { + "a": { "schema": "http://localhost:3100/graphql", "documents": "./a/**" }, + "b": { "schema": "./b/schema.ts", "documents": "./b/**" } } + } + }`, + ], + schemaFile, + ], + settings: { schemaCacheTTL: 500 }, + }); + + const initParams = await project.init('a/query.ts'); + expect(initParams.diagnostics[0].message).toEqual('Unknown fragment "T".'); + + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + expect(await project.lsp._graphQLCache.getSchema('a')).toBeDefined(); + + fetchMock.restore(); + mockSchema( + buildASTSchema( + parse( + 'type example100 { string: String } type Query { example: example100 }', + ), + ), + ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('a/fragments.ts'), type: FileChangeType.Changed }, + ], + }); + await sleep(1000); + expect( + (await project.lsp._graphQLCache.getSchema('a')).getType('example100'), + ).toBeTruthy(); + await sleep(1000); + const file = await readFile(join(genSchemaPath.replace('default', 'a')), { + encoding: 'utf-8', + }); + expect(file).toContain('example100'); + // add a new typescript file with empty query to the b project + // and expect autocomplete to only show options for project b + await project.addFile( + 'b/empty.ts', + 'import gql from "graphql-tag"\ngql`query a { }`', + ); + const completion = await project.lsp.handleCompletionRequest({ + textDocument: { uri: project.uri('b/empty.ts') }, + position: { character: 13, line: 1 }, + }); + + expect(completion.items?.length).toEqual(5); + expect(completion.items.map(i => i.label)).toEqual([ + 'foo', + 'test', + '__typename', + '__schema', + '__type', + ]); + // this confirms that autocomplete respects cross-project boundaries for types. + // it performs a definition request for the foo field in Query + const schemaCompletion1 = await project.lsp.handleCompletionRequest({ + textDocument: { uri: project.uri('b/schema.ts') }, + position: { character: 21, line: 3 }, + }); + expect(schemaCompletion1.items.map(i => i.label)).toEqual(['Foo']); + // it performs a definition request for the Foo type in Test.test + const schemaDefinition = await project.lsp.handleDefinitionRequest({ + textDocument: { uri: project.uri('b/schema.ts') }, + position: { character: 21, line: 6 }, + }); + expect(serializeRange(schemaDefinition[0].range)).toEqual( + fooInlineTypePosition, + ); + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + // simulate a watched schema file change (codegen, etc) + project.changeFile( + 'b/schema.ts', + `\n\nexport const schema = gql(\`\n${ + schemaFile[1] + '\ntype Example1 { field: }' + }\`\n)`, + ); + await project.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { uri: project.uri('b/schema.ts'), type: FileChangeType.Changed }, + ], + }); + // TODO: repeat this with other changes to the schema file and use a + // didChange event to see if the schema updates properly as well + // await project.lsp.handleDidChangeNotification({ + // textDocument: { uri: project.uri('b/schema.graphql'), version: 1 }, + // contentChanges: [ + // { text: schemaFile[1] + '\ntype Example1 { field: }' }, + // ], + // }); + // console.log(project.fileCache.get('b/schema.graphql')); + const schemaCompletion = await project.lsp.handleCompletionRequest({ + textDocument: { uri: project.uri('b/schema.ts') }, + position: { character: 25, line: 8 }, + }); + // TODO: SDL completion still feels incomplete here... where is Int? + // where is self-referential Example1? + expect(schemaCompletion.items.map(i => i.label)).toEqual([ + 'Query', + 'Foo', + 'String', + 'Test', + 'Boolean', + ]); + + expect(project.lsp._logger.error).not.toHaveBeenCalled(); + project.lsp.handleShutdownRequest(); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts new file mode 100644 index 00000000000..f7866010fa9 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.test.ts @@ -0,0 +1,979 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { SymbolKind } from 'vscode-languageserver'; +import { FileChangeType } from 'vscode-languageserver-protocol'; +import { Position, Range } from 'graphql-language-service'; + +import { + MessageProcessor, + processDiagnosticsMessage, +} from '../MessageProcessor'; +import { parseDocument } from '../parseDocument'; + +jest.mock('../Logger'); + +jest.setTimeout(20000); + +import { GraphQLCache } from '../GraphQLCache'; + +import { + ConfigInvalidError, + ConfigNotFoundError, + LoaderNoResultError, + ProjectNotFoundError, + loadConfig, +} from 'graphql-config'; + +import type { DefinitionQueryResult, Outline } from 'graphql-language-service'; + +import { NoopLogger } from '../Logger'; +import { pathToFileURL } from 'node:url'; +import mockfs from 'mock-fs'; +import { join } from 'node:path'; + +jest.mock('node:fs', () => ({ + ...jest.requireActual('fs'), + readFileSync: jest.fn(jest.requireActual('fs').readFileSync), +})); + +describe('MessageProcessor', () => { + const logger = new NoopLogger(); + const messageProcessor = new MessageProcessor({ + // @ts-ignore + connection: {}, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + config: null, + }); + + const queryPathUri = pathToFileURL(`${__dirname}/__queries__`); + const textDocumentTestString = ` + { + hero(episode: NEWHOPE){ + } + } + `; + let gqlConfig; + beforeEach(async () => { + gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); + + // loadConfig.mockRestore(); + messageProcessor._settings = { load: {} }; + messageProcessor._graphQLCache = new GraphQLCache({ + configDir: __dirname, + config: gqlConfig, + parser: parseDocument, + logger: new NoopLogger(), + }); + messageProcessor._languageService = { + // @ts-ignore + getAutocompleteSuggestions(query, position, uri) { + return [{ label: `${query} at ${uri}` }]; + }, + // @ts-ignore + getDiagnostics(_query, _uri) { + return []; + }, + async getHoverInformation(_query, position, _uri) { + return { + contents: '```graphql\nField: hero\n```', + range: new Range(position, position), + }; + }, + async getDocumentSymbols(_query: string, uri: string) { + return [ + { + name: 'item', + kind: SymbolKind.Field, + location: { + uri, + range: { + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }, + }, + }, + ]; + }, + async getOutline(_query: string): Promise { + return { + outlineTrees: [ + { + representativeName: 'item', + kind: 'Field', + startPosition: new Position(1, 2), + endPosition: new Position(1, 4), + children: [], + }, + ], + }; + }, + async getDefinition( + _query, + position, + uri, + ): Promise { + return { + queryRange: [new Range(position, position)], + printedName: 'example', + definitions: [ + { + position, + path: uri, + }, + ], + }; + }, + }; + }); + + let getConfigurationReturnValue = {}; + // @ts-ignore + messageProcessor._connection = { + // @ts-ignore + get workspace() { + return { + async getConfiguration() { + return [getConfigurationReturnValue]; + }, + }; + }, + }; + + const initialDocument = { + textDocument: { + text: textDocumentTestString, + uri: `${queryPathUri}/test.graphql`, + version: 0, + }, + }; + + messageProcessor._isInitialized = true; + + it('initializes properly and opens a file', async () => { + const { capabilities } = await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + expect(capabilities.definitionProvider).toEqual(true); + expect(capabilities.workspaceSymbolProvider).toEqual(true); + expect(capabilities.completionProvider.resolveProvider).toEqual(true); + expect(capabilities.textDocumentSync).toEqual(1); + }); + it('detects a config file', async () => { + const result = + await messageProcessor._isGraphQLConfigFile('graphql.config.js'); + expect(result).toEqual(true); + const falseResult = + await messageProcessor._isGraphQLConfigFile('graphql.js'); + expect(falseResult).toEqual(false); + + mockfs({ [`${__dirname}/package.json`]: '{"graphql": {}}' }); + const pkgResult = await messageProcessor._isGraphQLConfigFile( + `file://${__dirname}/package.json`, + ); + mockfs.restore(); + expect(pkgResult).toEqual(true); + + mockfs({ [`${__dirname}/package.json`]: '{ }' }); + const pkgFalseResult = await messageProcessor._isGraphQLConfigFile( + `file://${__dirname}/package.json`, + ); + mockfs.restore(); + expect(pkgFalseResult).toEqual(false); + }); + it('runs completion requests properly', async () => { + const uri = `${queryPathUri}/test2.graphql`; + const query = 'test'; + messageProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query, + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + + const test = { + position: new Position(0, 0), + textDocument: { uri }, + }; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual({ + items: [{ label: `${query} at ${uri}` }], + isIncomplete: false, + }); + }); + it('runs completion requests properly with no file present', async () => { + const test = { + position: new Position(0, 0), + textDocument: { uri: `${queryPathUri}/test13.graphql` }, + }; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual({ + items: [], + isIncomplete: false, + }); + }); + it('runs completion requests properly when not initialized', async () => { + const test = { + position: new Position(0, 3), + textDocument: { uri: `${queryPathUri}/test2.graphql` }, + }; + messageProcessor._isInitialized = false; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual({ + items: [], + isIncomplete: false, + }); + }); + + it('runs document symbol requests', async () => { + messageProcessor._isInitialized = true; + const uri = `${queryPathUri}/test3.graphql`; + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri, + version: 0, + }, + }; + + messageProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + + const test = { + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDocumentSymbolRequest(test); + + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('item'); + expect(result[0].kind).toEqual(SymbolKind.Field); + expect(result[0].location.range).toEqual({ + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }); + }); + it('runs document symbol requests with no file present', async () => { + const test = { + textDocument: { + uri: `${queryPathUri}/test4.graphql`, + version: 0, + }, + }; + + const result = await messageProcessor.handleDocumentSymbolRequest(test); + expect(result).toEqual([]); + }); + it('runs document symbol requests when not initialized', async () => { + const test = { + textDocument: { + uri: `${queryPathUri}/test3.graphql`, + version: 0, + }, + }; + messageProcessor._isInitialized = false; + const result = await messageProcessor.handleDocumentSymbolRequest(test); + expect(result).toEqual([]); + messageProcessor._isInitialized = true; + const nextResult = await messageProcessor.handleDocumentSymbolRequest(test); + expect(nextResult[0].location.uri).toContain('test3.graphql'); + expect(nextResult[0].name).toEqual('item'); + expect(nextResult.length).toEqual(1); + }); + + it('properly changes the file cache with the didChange handler', async () => { + const uri = `${queryPathUri}/test.graphql`; + messageProcessor._textDocumentCache.set(uri, { + version: 1, + contents: [ + { + query: '', + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + const textDocumentChangedString = ` + { + hero(episode: NEWHOPE){ + name + } + } + `; + + const result = await messageProcessor.handleDidChangeNotification({ + textDocument: { + // @ts-ignore + text: textDocumentTestString, + uri, + version: 1, + }, + contentChanges: [ + { text: textDocumentTestString }, + { text: textDocumentChangedString }, + ], + }); + // Query fixed, no more errors + expect(result.diagnostics.length).toEqual(0); + }); + + it('does not crash on null value returned in response to workspace configuration', async () => { + // for some reason this is needed? can't be a good thing... must have done something to cause a performance hit on + // loading config schema.. + jest.setTimeout(10000); + const previousConfigurationValue = getConfigurationReturnValue; + getConfigurationReturnValue = null; + const result = await messageProcessor.handleDidChangeConfiguration({}); + expect(result).toEqual({}); + getConfigurationReturnValue = previousConfigurationValue; + }); + + it('properly removes from the file cache with the didClose handler', async () => { + await messageProcessor.handleDidCloseNotification(initialDocument); + + const position = { line: 4, character: 5 }; + const params = { textDocument: initialDocument.textDocument, position }; + + // Should throw because file has been deleted from cache + try { + const result = await messageProcessor.handleCompletionRequest(params); + expect(result).toEqual(null); + } catch {} + }); + + // modified to work with jest.mock() of WatchmanClient + it('runs definition requests', async () => { + jest.setTimeout(10000); + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryPathUri}/test3.graphql`, + version: 1, + }, + }; + messageProcessor._getCachedDocument = (_uri: string) => ({ + version: 1, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(20, 4)), + }, + ], + }); + + await messageProcessor.handleDidOpenOrSaveNotification(newDocument); + + const test = { + position: new Position(3, 15), + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDefinitionRequest(test); + await expect(result[0].uri).toEqual(`${queryPathUri}/test3.graphql`); + }); + + it('retrieves custom results from locateCommand', async () => { + jest.setTimeout(10000); + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryPathUri}/test3.graphql`, + version: 1, + }, + }; + messageProcessor._getCachedDocument = (_uri: string) => ({ + version: 1, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(20, 4)), + }, + ], + }); + + await messageProcessor.handleDidOpenOrSaveNotification(newDocument); + + const test = { + position: new Position(3, 15), + textDocument: newDocument.textDocument, + }; + const result = await messageProcessor._languageService.getDefinition( + validQuery, + test.position, + test.textDocument.uri, + ); + const project = messageProcessor._graphQLCache.getProjectForFile( + test.textDocument.uri, + ); + + const customResult = messageProcessor._getCustomLocateResult( + project, + { definitions: result, printedName: 'example' }, + () => 'hello', + ); + expect(customResult.uri).toEqual('hello'); + + const customResult2 = messageProcessor._getCustomLocateResult( + project, + { definitions: result, printedName: 'example' }, + () => 'hello:2:4', + ); + expect(customResult2.uri).toEqual('hello'); + expect(customResult2.range.start.line).toEqual(2); + expect(customResult2.range.start.character).toEqual(0); + expect(customResult2.range.end.line).toEqual(4); + + const customResult3 = messageProcessor._getCustomLocateResult( + project, + { definitions: result, printedName: 'example' }, + () => ({ + uri: 'hello1', + range: { + start: { character: 2, line: 2 }, + end: { character: 4, line: 4 }, + }, + }), + ); + expect(customResult3.uri).toEqual('hello1'); + expect(customResult3.range.start.line).toEqual(2); + expect(customResult3.range.start.character).toEqual(2); + expect(customResult3.range.end.line).toEqual(4); + expect(customResult3.range.end.character).toEqual(4); + const oldGetProject = messageProcessor._graphQLCache.getProjectForFile; + + messageProcessor._graphQLCache.getProjectForFile = jest.fn(() => ({ + schema: project.schema, + documents: project.documents, + dirpath: project.dirpath, + extensions: { + languageService: { locateCommand: () => 'foo:3:4' }, + }, + })); + const result2 = await messageProcessor.handleDefinitionRequest(test); + expect(result2[0].range.start.line).toBe(3); + expect(result2[0].range.end.line).toBe(4); + expect(result2[0].range.end.character).toBe(0); + messageProcessor._graphQLCache.getProjectForFile = oldGetProject; + }); + it('runs hover requests', async () => { + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryPathUri}/test4.graphql`, + version: 1, + }, + }; + messageProcessor._getCachedDocument = (_uri: string) => ({ + version: 1, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(20, 4)), + }, + ], + }); + + await messageProcessor.handleDidOpenOrSaveNotification(newDocument); + + const test = { + position: new Position(3, 15), + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleHoverRequest(test); + expect(JSON.stringify(result.contents)).toEqual( + JSON.stringify({ + contents: '```graphql\nField: hero\n```', + range: new Range(new Position(3, 15), new Position(3, 15)), + }), + ); + }); + it('runs hover request with no file present', async () => { + const test = { + position: new Position(3, 15), + textDocument: { + uri: `${queryPathUri}/test5.graphql`, + version: 1, + }, + }; + messageProcessor._getCachedDocument = (_uri: string) => null; + + const result = await messageProcessor.handleHoverRequest(test); + expect(result).toEqual({ contents: [] }); + }); + it('handles provided config', async () => { + const msgProcessor = new MessageProcessor({ + // @ts-ignore + connection: { + workspace: { + getConfiguration() { + return {}; + }, + }, + }, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + config: gqlConfig, + }); + expect(msgProcessor._providedConfig).toBeTruthy(); + await msgProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + await msgProcessor.handleDidChangeConfiguration({ + settings: {}, + }); + expect(msgProcessor._graphQLCache).toBeTruthy(); + }); + + it('runs workspace symbol requests', async () => { + const msgProcessor = new MessageProcessor({ + // @ts-ignore + connection: {}, + logger, + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + }); + await msgProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + const uri = `${queryPathUri}/test6.graphql`; + const docUri = `${queryPathUri}/test7.graphql`; + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + const validDocument = ` + fragment testFragment on Character { + name + }`; + msgProcessor._graphQLCache = new GraphQLCache({ + configDir: __dirname, + config: await loadConfig({ rootDir: __dirname }), + parser: parseDocument, + logger: new NoopLogger(), + }); + msgProcessor._languageService = { + getDocumentSymbols: async () => [ + { + name: 'testFragment', + kind: SymbolKind.Field, + location: { + uri, + range: { + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }, + }, + }, + ], + }; + msgProcessor._isInitialized = true; + msgProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(6, 0)), + }, + ], + }); + + await msgProcessor._graphQLCache.updateFragmentDefinition( + __dirname, + docUri, + [ + { + query: validDocument, + range: new Range(new Position(0, 0), new Position(4, 0)), + }, + ], + ); + + const test = { + query: 'testFragment', + }; + + const result = await msgProcessor.handleWorkspaceSymbolRequest(test); + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('testFragment'); + expect(result[0].kind).toEqual(SymbolKind.Field); + expect(result[0].location.range).toEqual({ + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }); + }); + + describe('_loadConfigOrSkip', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._initializeGraphQLCaches = jest.fn(); + }); + + it('loads config if not initialized', async () => { + messageProcessor._isInitialized = false; + + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/graphql.config.js`, + ); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalledTimes( + 1, + ); + // we want to return true here to skip further processing, because it's just a config file change + expect(result).toEqual(true); + }); + + it('loads config if a file change occurs and the server is not initialized', async () => { + messageProcessor._isInitialized = false; + + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/file.ts`, + ); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalledTimes( + 1, + ); + // here we have a non-config file, so we don't want to skip, because we need to run diagnostics etc + expect(result).toEqual(false); + }); + it('config file change updates server config even if the server is already initialized', async () => { + messageProcessor._isInitialized = true; + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/graphql.config.ts`, + ); + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalledTimes( + 1, + ); + expect(result).toEqual(true); + }); + it('skips if the server is already initialized', async () => { + messageProcessor._isInitialized = true; + const result = await messageProcessor._loadConfigOrSkip( + `${pathToFileURL('.')}/myFile.ts`, + ); + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(result).toEqual(false); + }); + }); + + describe('handleDidOpenOrSaveNotification', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._initializeGraphQLCaches = jest.fn(); + messageProcessor._loadConfigOrSkip = jest.fn(); + }); + it('updates config for standard config filename changes', async () => { + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/.graphql.config.js`, + languageId: 'js', + version: 0, + text: '', + }, + }); + expect(messageProcessor._loadConfigOrSkip).toHaveBeenCalled(); + }); + + it('updates config for custom config filename changes', async () => { + const customConfigName = 'custom-config-name.yml'; + messageProcessor._settings = { load: { fileName: customConfigName } }; + + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/${customConfigName}`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._loadConfigOrSkip).toHaveBeenCalledWith( + expect.stringContaining(customConfigName), + ); + }); + + it('handles config requests with no config', async () => { + messageProcessor._settings = {}; + + await messageProcessor.handleDidChangeConfiguration({ + settings: [], + }); + + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalled(); + + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/.graphql.config.js`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._initializeGraphQLCaches).toHaveBeenCalled(); + }); + }); + + describe('_handleConfigErrors', () => { + it('handles missing config errors', async () => { + messageProcessor._handleConfigError({ + err: new ConfigNotFoundError('test missing-config'), + uri: 'test', + }); + + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('test missing-config'), + ); + }); + it('handles missing project errors', async () => { + messageProcessor._handleConfigError({ + err: new ProjectNotFoundError('test missing-project'), + uri: 'test', + }); + + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Project not found for this file'), + ); + }); + it('handles invalid config errors', async () => { + messageProcessor._handleConfigError({ + err: new ConfigInvalidError('test invalid error'), + uri: 'test', + }); + + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Invalid configuration'), + ); + }); + it('handles empty loader result errors', async () => { + messageProcessor._handleConfigError({ + err: new LoaderNoResultError('test loader-error'), + uri: 'test', + }); + + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('test loader-error'), + ); + }); + it('handles generic errors', async () => { + messageProcessor._handleConfigError({ + err: new Error('test loader-error'), + uri: 'test', + }); + + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('test loader-error'), + ); + }); + }); + describe('handleWatchedFilesChangedNotification', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(' query { id }'); + messageProcessor._initializeGraphQLCaches = jest.fn(); + messageProcessor._updateFragmentDefinition = jest.fn(); + messageProcessor._isGraphQLConfigMissing = false; + messageProcessor._isInitialized = true; + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: `${pathToFileURL( + join(__dirname, '__queries__'), + )}/test.graphql`, + type: FileChangeType.Changed, + }, + ], + }); + + expect(messageProcessor._initializeGraphQLCaches).not.toHaveBeenCalled(); + expect(messageProcessor._updateFragmentDefinition).toHaveBeenCalled(); + }); + }); + + describe('handleWatchedFilesChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._isGraphQLConfigMissing = true; + messageProcessor._parser = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: `${pathToFileURL('.')}/foo.js`, + type: FileChangeType.Changed, + }, + ], + }); + expect(messageProcessor._parser).not.toHaveBeenCalled(); + }); + }); + + describe('handleDidChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._isGraphQLConfigMissing = true; + messageProcessor._parser = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleDidChangeNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/foo.js`, + version: 1, + }, + contentChanges: [{ text: 'var something' }], + }); + expect(messageProcessor._parser).not.toHaveBeenCalled(); + }); + }); +}); + +describe('processDiagnosticsMessage', () => { + it('processes diagnostics messages', () => { + const query = 'query { foo }'; + const inputRange = new Range(new Position(1, 1), new Position(1, 1)); + + const diagnostics = processDiagnosticsMessage( + [ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: inputRange, + }, + ], + query, + inputRange, + ); + + expect(JSON.stringify(diagnostics)).toEqual( + JSON.stringify([ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: new Range(new Position(2, 1), new Position(2, 1)), + }, + ]), + ); + }); + it('processes diagnostics messages with null range', () => { + const query = 'query { foo }'; + const inputRange = new Range(new Position(1, 1), new Position(1, 1)); + + const diagnostics = processDiagnosticsMessage( + [ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: inputRange, + }, + ], + query, + null, + ); + + expect(JSON.stringify(diagnostics)).toEqual( + JSON.stringify([ + { + severity: 1, + message: 'test', + source: 'GraphQL: Validation', + range: inputRange, + }, + ]), + ); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts new file mode 100644 index 00000000000..68ab44d7de9 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/MockProject.ts @@ -0,0 +1,201 @@ +import mockfs from 'mock-fs'; +import { MessageProcessor } from '../../MessageProcessor'; +import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; +import { URI } from 'vscode-uri'; +import { FileChangeType } from 'vscode-languageserver'; +import { FileChangeTypeKind } from 'graphql-language-service'; + +export type MockFile = [filename: string, text: string]; + +export class MockLogger implements VSCodeLogger { + error = jest.fn(); + warn = jest.fn(); + info = jest.fn(); + log = jest.fn(); +} + +// when using mockfs with cosmic-config, a dynamic inline +// require of parse-json creates the necessity for loading in the actual +// modules to the mocked filesystem +const modules = [ + 'parse-json', + 'error-ex', + 'is-arrayish', + 'json-parse-even-better-errors', + 'lines-and-columns', + '@babel/code-frame', + '@babel/highlight', + // these i think are just required by jest when you console log from a test + 'jest-message-util', + 'stack-utils', + 'pretty-format', + 'ansi-regex', + 'js-tokens', + 'escape-string-regexp', + 'jest-worker', + 'jiti', + 'cosmiconfig', + 'minimatch', + 'tslib', +]; +const defaultMocks = modules.reduce((acc, module) => { + acc[`node_modules/${module}`] = mockfs.load(`node_modules/${module}`); + return acc; +}, {}); + +type File = [filename: string, text: string]; +type Files = File[]; + +export class MockProject { + private root: string; + private fileCache: Map; + private messageProcessor: MessageProcessor; + constructor({ + files = [], + root = '/tmp/test', + settings, + }: { + files: Files; + root?: string; + settings?: Record; + }) { + this.root = root; + this.fileCache = new Map(files); + + this.mockFiles(); + this.messageProcessor = new MessageProcessor({ + connection: { + get workspace() { + return { + async getConfiguration() { + return settings; + }, + }; + }, + }, + logger: new MockLogger(), + loadConfigOptions: { + rootDir: root, + }, + }); + } + + public async init(filename?: string, fileText?: string) { + await this.lsp.handleInitializeRequest({ + rootPath: this.root, + rootUri: this.root, + capabilities: {}, + processId: 200, + workspaceFolders: null, + }); + return this.lsp.handleDidOpenOrSaveNotification({ + textDocument: { + uri: this.uri(filename || 'query.graphql'), + version: 1, + text: + this.fileCache.get('query.graphql') || + (filename && this.fileCache.get(filename)) || + fileText, + }, + }); + } + private mockFiles() { + const mockFiles = { + ...defaultMocks, + // without this, the generated schema file may not be cleaned up by previous tests + '/tmp/graphql-language-service': mockfs.directory(), + }; + for (const [filename, text] of this.fileCache) { + mockFiles[this.filePath(filename)] = text; + } + mockfs(mockFiles); + } + public filePath(filename: string) { + return `${this.root}/${filename}`; + } + public uri(filename: string) { + return URI.file(this.filePath(filename)).toString(); + } + changeFile(filename: string, text: string) { + this.fileCache.set(filename, text); + this.mockFiles(); + } + async addFile(filename: string, text: string, watched = false) { + this.fileCache.set(filename, text); + this.mockFiles(); + if (watched) { + await this.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: this.uri(filename), + type: FileChangeTypeKind.Created, + }, + ], + }); + } + await this.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeTypeKind.Created, + text, + }, + ], + textDocument: { + uri: this.uri(filename), + version: 2, + }, + }); + } + async changeWatchedFile(filename: string, text: string) { + this.changeFile(filename, text); + await this.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: this.uri(filename), + type: FileChangeType.Changed, + }, + ], + }); + } + async saveOpenFile(filename: string, text: string) { + this.changeFile(filename, text); + await this.lsp.handleDidOpenOrSaveNotification({ + textDocument: { + uri: this.uri(filename), + version: 2, + text, + }, + }); + } + async addWatchedFile(filename: string, text: string) { + this.changeFile(filename, text); + await this.lsp.handleDidChangeNotification({ + contentChanges: [ + { + type: FileChangeTypeKind.Created, + text, + }, + ], + textDocument: { + uri: this.uri(filename), + version: 2, + }, + }); + } + async deleteFile(filename: string) { + mockfs.restore(); + this.fileCache.delete(filename); + this.mockFiles(); + await this.lsp.handleWatchedFilesChangedNotification({ + changes: [ + { + type: FileChangeType.Deleted, + uri: this.uri(filename), + }, + ], + }); + } + get lsp() { + return this.messageProcessor; + } +} diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js new file mode 100644 index 00000000000..0e328a55450 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/runServer.js @@ -0,0 +1 @@ +exports.default = require('../../../../graphiql/test/e2e-server.js'); diff --git a/packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts b/packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts new file mode 100644 index 00000000000..4ad1eff2c26 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__utils__/utils.ts @@ -0,0 +1,4 @@ +import { Range } from 'vscode-languageserver'; + +export const serializeRange = (range: Range) => + JSON.parse(JSON.stringify(range)); diff --git a/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags.test.ts similarity index 77% rename from packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts rename to packages/graphql-language-service-server/src/__tests__/findGraphQLTags.test.ts index 6bb5c1062bf..15759d8cc30 100644 --- a/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts +++ b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags.test.ts @@ -20,9 +20,9 @@ describe('findGraphQLTags', () => { const findGraphQLTags = (text: string, ext: SupportedExtensionsEnum) => baseFindGraphQLTags(text, ext, '', logger); - it('returns empty for files without asts', () => { + it('returns empty for files without asts', async () => { const text = '// just a comment'; - const contents = findGraphQLTags(text, '.js'); + const contents = await findGraphQLTags(text, '.js'); expect(contents.length).toEqual(0); }); @@ -45,7 +45,7 @@ query Test { export function Example(arg: string) {}`; - const contents = findGraphQLTags(text, '.js'); + const contents = await findGraphQLTags(text, '.js'); expect(contents[0].template).toEqual(` query Test { test { @@ -75,7 +75,7 @@ query Test { export function Example(arg: string) {}`; - const contents = findGraphQLTags(text, '.js'); + const contents = await findGraphQLTags(text, '.js'); expect(contents[0].template).toEqual(` query Test { test { @@ -83,6 +83,36 @@ query Test { ...FragmentsComment } } + + `); + }); + + it('finds queries in call expressions with with newlines preceding the template', async () => { + const text = ` + import {gql} from 'react-apollo'; + import type {B} from 'B'; + import A from './A'; + + const QUERY = gql( + \` + query Test { + test { + value + ...FragmentsComment + } + } + \`); + + export function Example(arg: string) {}`; + + const contents = await findGraphQLTags(text, '.ts'); + expect(contents[0].template).toEqual(` + query Test { + test { + value + ...FragmentsComment + } + } `); }); @@ -104,7 +134,7 @@ query Test { export function Example(arg: string) {}`; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(`#graphql query Test { test { @@ -112,6 +142,7 @@ query Test { ...FragmentsComment } } + `); }); @@ -122,9 +153,7 @@ import {B} from 'B'; import A from './A'; -const QUERY: string = -/* GraphQL */ -\` +const QUERY: string = /* GraphQL */ \` query Test { test { value @@ -136,7 +165,7 @@ query Test { export function Example(arg: string) {}`; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(` query Test { test { @@ -144,13 +173,14 @@ query Test { ...FragmentsComment } } + `); }); it('finds queries with nested graphql.experimental template tag expression', async () => { const text = 'const query = graphql.experimental` query {} `'; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(' query {} '); }); @@ -161,7 +191,7 @@ query Test { const query = graphql\` query {} \` `; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(' query {} '); }); @@ -212,7 +242,7 @@ class Todo2{} class AppModule {} const query = graphql\` query {} \` `; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(' query {} '); }); @@ -222,7 +252,7 @@ class Todo2{} else: () => gql\` query {} \` }`; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(' query {} '); }); @@ -231,7 +261,7 @@ class Todo2{} else: () => graphql\` query {} \` })`; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents[0].template).toEqual(' query {} '); }); @@ -243,7 +273,7 @@ query {id} \`; `; - const contents = findGraphQLTags(text, '.vue'); + const contents = await findGraphQLTags(text, '.vue'); expect(contents[0].template).toEqual(` query {id}`); expect(contents[0].range.start.line).toEqual(2); @@ -260,7 +290,7 @@ query {id} \`; `; - const contents = findGraphQLTags(text, '.vue'); + const contents = await findGraphQLTags(text, '.vue'); expect(contents[0].template).toEqual(` query {id}`); expect(contents[0].range.start.line).toEqual(4); @@ -275,7 +305,7 @@ query {id} \`; `; - const contents = findGraphQLTags(text, '.vue'); + const contents = await findGraphQLTags(text, '.vue'); expect(contents[0].template).toEqual(` query {id}`); expect(contents[0].range.start.line).toEqual(2); @@ -292,7 +322,7 @@ query {id} \`; `; - const contents = findGraphQLTags(text, '.vue'); + const contents = await findGraphQLTags(text, '.vue'); expect(contents[0].template).toEqual(` query {id}`); expect(contents[0].range.start.line).toEqual(4); @@ -316,7 +346,7 @@ export default defineComponent({ `; - const contents = findGraphQLTags(text, '.vue'); + const contents = await findGraphQLTags(text, '.vue'); expect(contents[0].template).toEqual(` query {id}`); }); @@ -347,7 +377,7 @@ query {id}`); `; - const contents = findGraphQLTags(text, '.svelte'); + const contents = await findGraphQLTags(text, '.svelte'); expect(contents[0].template).toEqual(` query AllCharacters { characters { @@ -372,7 +402,12 @@ query {id}`); .spyOn(process.stderr, 'write') .mockImplementation(() => true); - const contents = baseFindGraphQLTags(text, '.svelte', '', new NoopLogger()); + const contents = await baseFindGraphQLTags( + text, + '.svelte', + '', + new NoopLogger(), + ); // We should have no contents expect(contents).toMatchObject([]); @@ -389,7 +424,12 @@ query {id}`); .spyOn(process.stderr, 'write') .mockImplementation(() => true); - const contents = baseFindGraphQLTags(text, '.svelte', '', new NoopLogger()); + const contents = await baseFindGraphQLTags( + text, + '.svelte', + '', + new NoopLogger(), + ); // We should have no contents expect(contents).toMatchObject([]); @@ -406,7 +446,12 @@ query {id}`); .spyOn(process.stderr, 'write') .mockImplementation(() => true); - const contents = baseFindGraphQLTags(text, '.svelte', '', new NoopLogger()); + const contents = await baseFindGraphQLTags( + text, + '.svelte', + '', + new NoopLogger(), + ); // We should have no contents expect(contents).toMatchObject([]); @@ -422,7 +467,7 @@ query {id}`); }) const query = graphql\`query myQuery {}\``; - const contents = findGraphQLTags(text, '.ts'); + const contents = await findGraphQLTags(text, '.ts'); expect(contents.length).toEqual(2); @@ -461,7 +506,7 @@ query Test { export function Example(arg: string) {}`; - const contents = findGraphQLTags(text, '.js'); + const contents = await findGraphQLTags(text, '.js'); expect(contents.length).toEqual(0); }); @@ -484,10 +529,10 @@ query Test { export function Example(arg: string) {}`; - const contents = findGraphQLTags(text, '.js'); + const contents = await findGraphQLTags(text, '.js'); expect(contents.length).toEqual(0); }); - it('handles full svelte example', () => { + it('handles full svelte example', async () => { const text = `