├── .changeset ├── README.md ├── clean-doors-move.md └── config.json ├── .gitattributes ├── .github ├── actions │ └── discord-message │ │ ├── action.mjs │ │ └── action.yml └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── package.json ├── packages ├── example-external-generator │ ├── .vscode │ │ └── settings.json │ ├── codegen.ts │ ├── package.json │ ├── schema.graphql │ ├── src │ │ ├── Pokemon.tsx │ │ ├── gql │ │ │ ├── fragment-masking.ts │ │ │ ├── gql.ts │ │ │ ├── graphql.ts │ │ │ └── index.ts │ │ └── index.tsx │ └── tsconfig.json ├── example-tada │ ├── .vscode │ │ └── settings.json │ ├── introspection.d.ts │ ├── package.json │ ├── schema.graphql │ ├── src │ │ ├── Pokemon.tsx │ │ ├── graphql.ts │ │ └── index.tsx │ └── tsconfig.json ├── example │ ├── .vscode │ │ └── settings.json │ ├── __generated__ │ │ └── baseGraphQLSP.ts │ ├── package.json │ ├── schema.graphql │ ├── src │ │ ├── Pokemon.generated.ts │ │ ├── Pokemon.ts │ │ ├── index.generated.ts │ │ └── index.ts │ └── tsconfig.json └── graphqlsp │ ├── .npmignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── api.ts │ ├── ast │ │ ├── checks.ts │ │ ├── cursor.ts │ │ ├── index.ts │ │ ├── resolve.ts │ │ ├── templates.ts │ │ └── token.ts │ ├── autoComplete.ts │ ├── checkImports.ts │ ├── diagnostics.ts │ ├── fieldUsage.ts │ ├── graphql │ │ ├── getFragmentSpreadSuggestions.ts │ │ └── getSchema.ts │ ├── index.ts │ ├── persisted.ts │ ├── quickInfo.ts │ └── ts │ │ ├── index.d.ts │ │ └── index.js │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── changelog.js ├── launch-debug.sh └── rollup.config.mjs ├── test └── e2e │ ├── client-preset.test.ts │ ├── combinations.test.ts │ ├── fixture-project-client-preset │ ├── .vscode │ │ └── settings.json │ ├── __generated__ │ │ └── baseGraphQLSP.ts │ ├── fixtures │ │ ├── fragment.ts │ │ ├── gql │ │ │ ├── fragment-masking.ts │ │ │ ├── gql.ts │ │ │ ├── graphql.ts │ │ │ └── index.ts │ │ ├── simple.ts │ │ └── unused-fragment.ts │ ├── package.json │ ├── schema.graphql │ └── tsconfig.json │ ├── fixture-project-tada-multi-schema │ ├── .vscode │ │ └── settings.json │ ├── fixtures │ │ ├── pokemon.ts │ │ ├── simple-pokemon.ts │ │ ├── simple-todo.ts │ │ ├── star-import.ts │ │ └── todo.ts │ ├── package.json │ ├── pokemon.ts │ ├── pokemons.d.ts │ ├── pokemons.graphql │ ├── todo.ts │ ├── todos.d.ts │ ├── todos.graphql │ └── tsconfig.json │ ├── fixture-project-tada │ ├── .vscode │ │ └── settings.json │ ├── fixtures │ │ ├── fragment.ts │ │ ├── graphql.ts │ │ ├── simple.ts │ │ ├── type-condition.ts │ │ └── unused-fragment.ts │ ├── graphql.ts │ ├── introspection.d.ts │ ├── package.json │ ├── schema.graphql │ └── tsconfig.json │ ├── fixture-project-unused-fields │ ├── .vscode │ │ └── settings.json │ ├── __generated__ │ │ └── baseGraphQLSP.ts │ ├── fixtures │ │ ├── bail.tsx │ │ ├── chained-usage.ts │ │ ├── destructuring.tsx │ │ ├── fragment-destructuring.tsx │ │ ├── fragment.tsx │ │ ├── gql │ │ │ ├── fragment-masking.ts │ │ │ ├── gql.ts │ │ │ ├── graphql.ts │ │ │ └── index.ts │ │ ├── immediate-destructuring.tsx │ │ └── property-access.tsx │ ├── gql │ │ ├── fragment-masking.ts │ │ ├── gql.ts │ │ ├── graphql.ts │ │ └── index.ts │ ├── package.json │ ├── schema.graphql │ └── tsconfig.json │ ├── fixture-project │ ├── .vscode │ │ └── settings.json │ ├── fixtures │ │ ├── Combination.ts │ │ ├── Post.ts │ │ ├── Posts.ts │ │ ├── rename-complex.ts │ │ ├── rename.ts │ │ └── simple.ts │ ├── package.json │ ├── schema.graphql │ └── tsconfig.json │ ├── graphqlsp.test.ts │ ├── multi-schema-tada.test.ts │ ├── server.ts │ ├── tada.test.ts │ ├── tsconfig.json │ ├── unused-fieds.test.ts │ └── util.ts └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/clean-doors-move.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@0no-co/graphqlsp': minor 3 | --- 4 | 5 | Remove missing operation-name code, with our increased focus on not generating any code this becomes irrelevant 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "../scripts/changelog.js", 4 | "commit": false, 5 | "access": "public", 6 | "baseBranch": "main", 7 | "ignore": ["example", "fixtures"], 8 | "updateInternalDependencies": "minor", 9 | "snapshot": { 10 | "prereleaseTemplate": "{tag}-{commit}", 11 | "useCalculatedVersion": true 12 | }, 13 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 14 | "onlyUpdatePeerDependentsWhenOutOfRange": true, 15 | "updateInternalDependents": "out-of-range" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | **/*.generated.ts linguist-generated 3 | **/*.graphql linguist-generated 4 | -------------------------------------------------------------------------------- /.github/actions/discord-message/action.mjs: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as github from '@actions/github'; 3 | 4 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN; 5 | const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL; 6 | 7 | const octokit = github.getOctokit(GITHUB_TOKEN); 8 | 9 | const formatBody = (input) => { 10 | const titleRe = /(?:^|\n)#+[^\n]+/g; 11 | const updatedDepsRe = /\n-\s*Updated dependencies[\s\S]+\n(\n\s+-[\s\S]+)*/gi; 12 | const markdownLinkRe = /\[([^\]]+)\]\(([^\)]+)\)/g; 13 | const creditRe = new RegExp(`Submitted by (?:undefined|${markdownLinkRe.source})`, 'ig'); 14 | const repeatedNewlineRe = /(?:\n[ ]*)*(\n[ ]*)/g; 15 | return input 16 | .replace(titleRe, '') 17 | .replace(updatedDepsRe, '') 18 | .replace(creditRe, (_match, text, url) => { 19 | if (!text || /@kitten|@JoviDeCroock/i.test(text)) return ''; 20 | return `Submitted by [${text}](${url})`; 21 | }) 22 | .replace(markdownLinkRe, (_match, text, url) => `[${text}](<${url}>)`) 23 | .replace(repeatedNewlineRe, (_match, text) => text ? ` ${text}` : '\n') 24 | .trim(); 25 | }; 26 | 27 | async function getReleaseBody(name, version) { 28 | const tag = `${name}@${version}`; 29 | const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); 30 | const result = await octokit.rest.repos.getReleaseByTag({ owner, repo, tag }); 31 | 32 | const release = result.status === 200 ? result.data : undefined; 33 | if (!release || !release.body) return; 34 | 35 | const title = `:package: [${tag}](<${release.html_url}>)`; 36 | const body = formatBody(release.body); 37 | if (!body) return; 38 | 39 | return `${title}\n${body}`; 40 | } 41 | 42 | async function main() { 43 | const inputPackages = core.getInput('publishedPackages'); 44 | let packages; 45 | 46 | try { 47 | packages = JSON.parse(inputPackages); 48 | } catch (e) { 49 | console.error('invalid JSON in publishedPackages input.'); 50 | return; 51 | } 52 | 53 | // Get releases 54 | const releasePromises = packages.map((entry) => { 55 | return getReleaseBody(entry.name, entry.version); 56 | }); 57 | 58 | const content = (await Promise.allSettled(releasePromises)) 59 | .map((x) => x.status === 'fulfilled' && x.value) 60 | .filter(Boolean) 61 | .join('\n\n'); 62 | 63 | // Send message through a discord webhook or bot 64 | const response = await fetch(WEBHOOK_URL, { 65 | method: 'POST', 66 | headers: { 67 | 'Content-Type': 'application/json', 68 | }, 69 | body: JSON.stringify({ content }), 70 | }); 71 | 72 | if (!response.ok) { 73 | console.error('Something went wrong while sending the discord webhook.', response.status); 74 | console.error(await response.text()); 75 | } 76 | } 77 | 78 | main().then().catch(console.error); 79 | -------------------------------------------------------------------------------- /.github/actions/discord-message/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Send a discord message' 2 | description: 'Send a discord message as a result of a gql.tada publish.' 3 | inputs: 4 | publishedPackages: 5 | description: > 6 | A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]` 7 | runs: 8 | using: 'node20' 9 | main: 'action.mjs' 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | check: 11 | name: Checks 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup Node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | 25 | - name: Setup pnpm 26 | uses: pnpm/action-setup@v3 27 | with: 28 | version: 8.6.1 29 | run_install: false 30 | 31 | - name: Get pnpm store directory 32 | id: pnpm-store 33 | run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 34 | 35 | - name: Use pnpm store 36 | uses: actions/cache@v4 37 | id: pnpm-cache 38 | with: 39 | path: ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} 40 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm- 43 | 44 | - name: Install Dependencies 45 | run: pnpm install --frozen-lockfile --prefer-offline 46 | 47 | - name: Build 48 | run: pnpm --filter @0no-co/graphqlsp run build 49 | 50 | - name: Test 51 | run: pnpm run test:e2e 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-20.04 10 | timeout-minutes: 20 11 | permissions: 12 | contents: write 13 | id-token: write 14 | issues: write 15 | repository-projects: write 16 | deployments: write 17 | packages: write 18 | pull-requests: write 19 | steps: 20 | - name: Checkout Repo 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | 30 | - name: Setup pnpm 31 | uses: pnpm/action-setup@v3 32 | with: 33 | version: 8.6.1 34 | run_install: false 35 | 36 | - name: Get pnpm store directory 37 | id: pnpm-store 38 | run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT 39 | 40 | - name: Use pnpm store 41 | uses: actions/cache@v4 42 | id: pnpm-cache 43 | with: 44 | path: ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} 45 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-pnpm- 48 | 49 | - name: Install Dependencies 50 | run: pnpm install --frozen-lockfile --prefer-offline 51 | 52 | - name: PR or Publish 53 | id: changesets 54 | uses: changesets/action@v1.4.6 55 | with: 56 | publish: pnpm changeset publish 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | 61 | - name: Notify discord 62 | id: discord-msg 63 | if: steps.changesets.outputs.published == 'true' 64 | uses: ./.github/actions/discord-message 65 | with: 66 | publishedPackages: ${{ steps.changesets.outputs.publishedPackages }} 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 70 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} 71 | 72 | - name: Publish Prerelease 73 | if: steps.changesets.outputs.published != 'true' 74 | continue-on-error: true 75 | env: 76 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | run: | 79 | npm config set "//registry.npmjs.org/:_authToken" "$NPM_TOKEN" 80 | git reset --hard origin/main 81 | pnpm changeset version --no-git-tag --snapshot canary 82 | pnpm changeset publish --no-git-tag --snapshot canary --tag canary 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | src/**/*.js 9 | example/src/**/*.js 10 | test/e2e/fixture-project/__generated__/* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | packages/graphqlsp/api/* 110 | packages/graphqlsp/api 111 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/e2e/fixture-project/fixtures/**/* 2 | ./**/*.generated.ts 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | // See: https://github.com/microsoft/TypeScript/wiki/Debugging-Language-Service-in-VS-Code 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attach to VS Code TS Server via Port", 12 | "port": 9559 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQLSP 2 | 3 | This is a TypeScript LSP Plugin that will recognise documents in your 4 | TypeScript code and help you out with hover-information, diagnostics and 5 | auto-complete. 6 | 7 | ## Features 8 | 9 | - Hover information showing the decriptions of fields 10 | - Diagnostics for adding fields that don't exist, are deprecated, missmatched argument types, ... 11 | - Auto-complete inside your editor for fields 12 | - Will warn you when you are importing from a file that is exporting fragments that you're not using 13 | 14 | > Note that this plugin does not do syntax highlighting, for that you still need something like 15 | > [the VSCode/... plugin](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax) 16 | 17 | ## Installation 18 | 19 | ```sh 20 | npm install -D @0no-co/graphqlsp 21 | ``` 22 | 23 | ## Usage 24 | 25 | Go to your `tsconfig.json` and add 26 | 27 | ```json 28 | { 29 | "compilerOptions": { 30 | "plugins": [ 31 | { 32 | "name": "@0no-co/graphqlsp", 33 | "schema": "./schema.graphql" 34 | } 35 | ] 36 | } 37 | } 38 | ``` 39 | 40 | now restart your TS-server and you should be good to go, ensure you are using the 41 | workspace version of TypeScript. In VSCode you can do so by clicking the bottom right 42 | when on a TypeScript file or adding a file like [this](https://github.com/0no-co/GraphQLSP/blob/main/packages/example/.vscode/settings.json). 43 | 44 | > If you are using VSCode ensure that your editor is using [the Workspace Version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript) 45 | > this can be done by manually selecting it or adding a `.vscode/config.json` with the contents of 46 | > 47 | > ```json 48 | > { 49 | > "typescript.tsdk": "node_modules/typescript/lib", 50 | > "typescript.enablePromptUseWorkspaceTsdk": true 51 | > } 52 | > ``` 53 | 54 | ### Configuration 55 | 56 | **Required** 57 | 58 | - `schema` allows you to specify a url, `.json` or `.graphql` file as your schema. If you need to specify headers for your introspection 59 | you can opt into the object notation i.e. `{ "schema": { "url": "x", "headers": { "Authorization": "y" } }}` 60 | 61 | **Optional** 62 | 63 | - `template` add an additional template to the defaults `gql` and `graphql` 64 | - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` (default: true) 65 | when using `false` it will look for tagged template literals 66 | - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 67 | unused fragments and provide a message notifying you about them (only works with call-expressions, default: true) 68 | - `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about 69 | unused fields within the same file. (only works with call-expressions, default: true) 70 | - `tadaOutputLocation` when using `gql.tada` this can be convenient as it automatically generates 71 | an `introspection.ts` file for you, just give it the directory to output to and you're done 72 | - `tadaDisablePreprocessing` this setting disables the optimisation of `tadaOutput` to a pre-processed TypeScript type, this is off by default. 73 | 74 | ## Tracking unused fields 75 | 76 | Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost 77 | it will only track the result and the accessed properties in the same file to encourage 78 | [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments). 79 | 80 | Secondly, we don't track mutations/subscriptions as some folks will add additional fields to properly support 81 | normalised cache updates. 82 | 83 | ## Fragment masking 84 | 85 | When we use a `useQuery` that supports `TypedDocumentNode` it will automatically pick up the typings 86 | from the `query` you provide it. However for fragments this could become a bit more troublesome, the 87 | minimal way of providing typings for a fragment would be the following: 88 | 89 | ```tsx 90 | import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 91 | 92 | export const PokemonFields = gql` 93 | fragment pokemonFields on Pokemon { 94 | id 95 | name 96 | } 97 | ` as typeof import('./Pokemon.generated').PokemonFieldsFragmentDoc; 98 | 99 | export const Pokemon = props => { 100 | const pokemon = useFragment(props.pokemon, PokemonFields); 101 | }; 102 | 103 | export function useFragment( 104 | data: any, 105 | _fragment: TypedDocumentNode 106 | ): Type { 107 | return data; 108 | } 109 | ``` 110 | 111 | This is mainly needed in cases where this isn't supported out of the box and mainly serves as a way 112 | for you to case your types. 113 | 114 | ## 💙 [Sponsors](https://github.com/sponsors/urql-graphql) 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
BigCommerce
BigCommerce
WunderGraph
WunderGraph
The Guild
The Guild
123 | 124 | 125 | 126 | 127 | 128 |
BeatGig
BeatGig
129 | 130 | ## Local development 131 | 132 | Run `pnpm i` at the root. Open `packages/example` by running `code packages/example` or if you want to leverage 133 | breakpoints do it with the `TSS_DEBUG_BRK=9559` prefix. When you make changes in `packages/graphqlsp` all you need 134 | to do is run `pnpm i` in your other editor and restart the `TypeScript server` for the changes to apply. 135 | 136 | > Ensure that both instances of your editor are using the Workspace Version of TypeScript 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphqlsp", 3 | "version": "0.2.0", 4 | "description": "TypeScript LSP plugin that finds GraphQL documents in your code and provides hints and auto-generates types.", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.module.js", 7 | "scripts": { 8 | "build": "pnpm --filter @0no-co/graphqlsp build", 9 | "prepare": "husky install", 10 | "dev": "pnpm --filter @0no-co/graphqlsp dev", 11 | "launch-debug": "./scripts/launch-debug.sh", 12 | "test:e2e": "vitest run --single-thread" 13 | }, 14 | "prettier": { 15 | "singleQuote": true, 16 | "arrowParens": "avoid", 17 | "trailingComma": "es5" 18 | }, 19 | "lint-staged": { 20 | "*.{js,ts,json,md}": "prettier --write" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged" 25 | } 26 | }, 27 | "devDependencies": { 28 | "@actions/core": "^1.10.0", 29 | "@actions/github": "^5.1.1", 30 | "@babel/plugin-transform-block-scoping": "^7.23.4", 31 | "@babel/plugin-transform-typescript": "^7.23.6", 32 | "@changesets/cli": "^2.26.2", 33 | "@changesets/get-github-info": "^0.5.2", 34 | "@rollup/plugin-babel": "^6.0.4", 35 | "@rollup/plugin-commonjs": "^25.0.7", 36 | "@rollup/plugin-node-resolve": "^15.2.3", 37 | "@rollup/plugin-terser": "^0.4.4", 38 | "@rollup/plugin-typescript": "^11.1.5", 39 | "@types/node": "^18.15.11", 40 | "dotenv": "^16.0.3", 41 | "husky": "^8.0.3", 42 | "lint-staged": "^15.0.0", 43 | "prettier": "^2.8.7", 44 | "rollup": "^4.9.5", 45 | "rollup-plugin-cjs-check": "^1.0.3", 46 | "rollup-plugin-dts": "^6.1.0", 47 | "typescript": "^5.3.3", 48 | "vitest": "^0.34.6" 49 | }, 50 | "pnpm": { 51 | "overrides": { 52 | "typescript": "^5.3.3", 53 | "ua-parser-js@<0.7.33": ">=0.7.33", 54 | "postcss@<8.4.31": ">=8.4.31", 55 | "semver@<5.7.2": ">=5.7.2", 56 | "semver@>=6.0.0 <6.3.1": ">=6.3.1", 57 | "vite@>=4.2.0 <4.2.3": ">=4.2.3", 58 | "json5@>=2.0.0 <2.2.2": ">=2.2.2", 59 | "@babel/traverse@<7.23.2": ">=7.23.2" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/example-external-generator/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/example-external-generator/codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | schema: './schema.graphql', 5 | documents: ['src/**/*.tsx'], 6 | ignoreNoDocuments: true, // for better experience with the watcher 7 | generates: { 8 | './src/gql/': { 9 | preset: 'client', 10 | }, 11 | }, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /packages/example-external-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@graphql-typed-document-node/core": "^3.2.0", 14 | "@urql/core": "^3.0.0", 15 | "graphql": "^16.8.1", 16 | "urql": "^4.0.6" 17 | }, 18 | "devDependencies": { 19 | "@0no-co/graphqlsp": "file:../graphqlsp", 20 | "@graphql-codegen/cli": "^5.0.0", 21 | "@graphql-codegen/client-preset": "^4.1.0", 22 | "@types/react": "^18.2.45", 23 | "ts-node": "^10.9.1", 24 | "typescript": "^5.3.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/example-external-generator/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /packages/example-external-generator/src/Pokemon.tsx: -------------------------------------------------------------------------------- 1 | import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 2 | import { graphql } from './gql'; 3 | 4 | export const PokemonFields = graphql(` 5 | fragment pokemonFields on Pokemon { 6 | id 7 | name 8 | attacks { 9 | fast { 10 | damage 11 | name 12 | } 13 | } 14 | } 15 | `) 16 | 17 | export const Pokemon = (data: any) => { 18 | const pokemon = useFragment(PokemonFields, data); 19 | return `hi ${pokemon.name}`; 20 | }; 21 | 22 | type X = { hello: string }; 23 | 24 | const x: X = { hello: '' }; 25 | 26 | export function useFragment( 27 | _fragment: TypedDocumentNode, 28 | data: any 29 | ): Type { 30 | return data; 31 | } 32 | -------------------------------------------------------------------------------- /packages/example-external-generator/src/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResultOf, 3 | DocumentTypeDecoration, 4 | TypedDocumentNode, 5 | } from '@graphql-typed-document-node/core'; 6 | import { FragmentDefinitionNode } from 'graphql'; 7 | import { Incremental } from './graphql'; 8 | 9 | export type FragmentType< 10 | TDocumentType extends DocumentTypeDecoration 11 | > = TDocumentType extends DocumentTypeDecoration 12 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 13 | ? TKey extends string 14 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 15 | : never 16 | : never 17 | : never; 18 | 19 | // return non-nullable if `fragmentType` is non-nullable 20 | export function useFragment( 21 | _documentNode: DocumentTypeDecoration, 22 | fragmentType: FragmentType> 23 | ): TType; 24 | // return nullable if `fragmentType` is nullable 25 | export function useFragment( 26 | _documentNode: DocumentTypeDecoration, 27 | fragmentType: 28 | | FragmentType> 29 | | null 30 | | undefined 31 | ): TType | null | undefined; 32 | // return array of non-nullable if `fragmentType` is array of non-nullable 33 | export function useFragment( 34 | _documentNode: DocumentTypeDecoration, 35 | fragmentType: ReadonlyArray>> 36 | ): ReadonlyArray; 37 | // return array of nullable if `fragmentType` is array of nullable 38 | export function useFragment( 39 | _documentNode: DocumentTypeDecoration, 40 | fragmentType: 41 | | ReadonlyArray>> 42 | | null 43 | | undefined 44 | ): ReadonlyArray | null | undefined; 45 | export function useFragment( 46 | _documentNode: DocumentTypeDecoration, 47 | fragmentType: 48 | | FragmentType> 49 | | ReadonlyArray>> 50 | | null 51 | | undefined 52 | ): TType | ReadonlyArray | null | undefined { 53 | return fragmentType as any; 54 | } 55 | 56 | export function makeFragmentData< 57 | F extends DocumentTypeDecoration, 58 | FT extends ResultOf 59 | >(data: FT, _fragment: F): FragmentType { 60 | return data as FragmentType; 61 | } 62 | export function isFragmentReady( 63 | queryNode: DocumentTypeDecoration, 64 | fragmentNode: TypedDocumentNode, 65 | data: 66 | | FragmentType, any>> 67 | | null 68 | | undefined 69 | ): data is FragmentType { 70 | const deferredFields = ( 71 | queryNode as { 72 | __meta__?: { deferredFields: Record }; 73 | } 74 | ).__meta__?.deferredFields; 75 | 76 | if (!deferredFields) return true; 77 | 78 | const fragDef = fragmentNode.definitions[0] as 79 | | FragmentDefinitionNode 80 | | undefined; 81 | const fragName = fragDef?.name?.value; 82 | 83 | const fields = (fragName && deferredFields[fragName]) || []; 84 | return fields.length > 0 && fields.every(field => data && field in data); 85 | } 86 | -------------------------------------------------------------------------------- /packages/example-external-generator/src/gql/gql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as types from './graphql'; 3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | 5 | /** 6 | * Map of all GraphQL operations in the project. 7 | * 8 | * This map has several performance disadvantages: 9 | * 1. It is not tree-shakeable, so it will include all operations in the project. 10 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 | * 3. It does not support dead code elimination, so it will add unused operations. 12 | * 13 | * Therefore it is highly recommended to use the babel or swc plugin for production. 14 | */ 15 | const documents = { 16 | '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 | types.PokemonFieldsFragmentDoc, 18 | '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n': 19 | types.PoDocument, 20 | }; 21 | 22 | /** 23 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 24 | * 25 | * 26 | * @example 27 | * ```ts 28 | * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 29 | * ``` 30 | * 31 | * The query argument is unknown! 32 | * Please regenerate the types. 33 | */ 34 | export function graphql(source: string): unknown; 35 | 36 | /** 37 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 38 | */ 39 | export function graphql( 40 | source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n' 41 | ): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n']; 42 | /** 43 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 44 | */ 45 | export function graphql( 46 | source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n' 47 | ): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n']; 48 | 49 | export function graphql(source: string) { 50 | return (documents as any)[source] ?? {}; 51 | } 52 | 53 | export type DocumentType> = 54 | TDocumentNode extends DocumentNode ? TType : never; 55 | -------------------------------------------------------------------------------- /packages/example-external-generator/src/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-masking'; 2 | export * from './gql'; 3 | -------------------------------------------------------------------------------- /packages/example-external-generator/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createClient, useQuery } from 'urql'; 2 | import { graphql } from './gql'; 3 | import { Pokemon } from './Pokemon'; 4 | 5 | const PokemonQuery = graphql(` 6 | query Po($id: ID!) { 7 | pokemon(id: $id) { 8 | id 9 | fleeRate 10 | ...pokemonFields 11 | attacks { 12 | special { 13 | name 14 | damage 15 | } 16 | } 17 | weight { 18 | minimum 19 | maximum 20 | } 21 | name 22 | __typename 23 | } 24 | } 25 | `); 26 | 27 | const Pokemons = () => { 28 | const [result] = useQuery({ 29 | query: PokemonQuery, 30 | variables: { id: '' } 31 | }); 32 | 33 | // Works 34 | console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name) 35 | 36 | // Works 37 | const { fleeRate } = result.data?.pokemon || {}; 38 | console.log(fleeRate) 39 | // Works 40 | const po = result.data?.pokemon; 41 | // @ts-expect-error 42 | const { pokemon: { weight: { minimum } } } = result.data || {}; 43 | console.log(po?.name, minimum) 44 | 45 | // Works 46 | const { pokemon } = result.data || {}; 47 | console.log(pokemon?.weight?.maximum) 48 | 49 | return ; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /packages/example-external-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schema": "./schema.graphql", 7 | "disableTypegen": true, 8 | "shouldCheckForColocatedFragments": true, 9 | "trackFieldUsage": true 10 | } 11 | ], 12 | "jsx": "react-jsx", 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | /* Modules */ 16 | "module": "commonjs" /* Specify what module code is generated. */, 17 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 18 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 19 | /* Type Checking */ 20 | "strict": true /* Enable all strict type-checking options. */, 21 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/example-tada/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/example-tada/introspection.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | 4 | /** An IntrospectionQuery representation of your schema. 5 | * 6 | * @remarks 7 | * This is an introspection of your schema saved as a file by GraphQLSP. 8 | * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents. 9 | * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to 10 | * instead save to a .ts instead of a .d.ts file. 11 | */ 12 | export type introspection = { 13 | name: 'pokemons'; 14 | query: 'Query'; 15 | mutation: never; 16 | subscription: never; 17 | types: { 18 | 'Attack': { kind: 'OBJECT'; name: 'Attack'; fields: { 'damage': { name: 'damage'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'type': { name: 'type'; type: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; } }; }; }; 19 | 'AttacksConnection': { kind: 'OBJECT'; name: 'AttacksConnection'; fields: { 'fast': { name: 'fast'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; 'special': { name: 'special'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; }; }; 20 | 'Boolean': unknown; 21 | 'EvolutionRequirement': { kind: 'OBJECT'; name: 'EvolutionRequirement'; fields: { 'amount': { name: 'amount'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 22 | 'Float': unknown; 23 | 'ID': unknown; 24 | 'Int': unknown; 25 | 'Pokemon': { kind: 'OBJECT'; name: 'Pokemon'; fields: { 'attacks': { name: 'attacks'; type: { kind: 'OBJECT'; name: 'AttacksConnection'; ofType: null; } }; 'classification': { name: 'classification'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'evolutionRequirements': { name: 'evolutionRequirements'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'EvolutionRequirement'; ofType: null; }; } }; 'evolutions': { name: 'evolutions'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; 'fleeRate': { name: 'fleeRate'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'height': { name: 'height'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'maxCP': { name: 'maxCP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'maxHP': { name: 'maxHP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'resistant': { name: 'resistant'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'types': { name: 'types'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weaknesses': { name: 'weaknesses'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weight': { name: 'weight'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; }; }; 26 | 'PokemonDimension': { kind: 'OBJECT'; name: 'PokemonDimension'; fields: { 'maximum': { name: 'maximum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'minimum': { name: 'minimum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 27 | 'PokemonType': { name: 'PokemonType'; enumValues: 'Bug' | 'Dark' | 'Dragon' | 'Electric' | 'Fairy' | 'Fighting' | 'Fire' | 'Flying' | 'Ghost' | 'Grass' | 'Ground' | 'Ice' | 'Normal' | 'Poison' | 'Psychic' | 'Rock' | 'Steel' | 'Water'; }; 28 | 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'pokemon': { name: 'pokemon'; type: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; } }; 'pokemons': { name: 'pokemons'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; }; }; 29 | 'String': unknown; 30 | }; 31 | }; 32 | 33 | import * as gqlTada from 'gql.tada'; 34 | -------------------------------------------------------------------------------- /packages/example-tada/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@graphql-typed-document-node/core": "^3.2.0", 14 | "gql.tada": "1.6.0", 15 | "@urql/core": "^3.0.0", 16 | "graphql": "^16.8.1", 17 | "urql": "^4.0.6" 18 | }, 19 | "devDependencies": { 20 | "@0no-co/graphqlsp": "file:../graphqlsp", 21 | "@graphql-codegen/cli": "^5.0.0", 22 | "@graphql-codegen/client-preset": "^4.1.0", 23 | "@types/react": "^18.2.45", 24 | "ts-node": "^10.9.1", 25 | "typescript": "^5.3.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/example-tada/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /packages/example-tada/src/Pokemon.tsx: -------------------------------------------------------------------------------- 1 | import { FragmentOf, graphql, readFragment } from './graphql'; 2 | 3 | export const Fields = { Pokemon: graphql(` 4 | fragment Pok on Pokemon { 5 | resistant 6 | types 7 | }`) 8 | } 9 | 10 | export const PokemonFields = graphql(/* GraphQL */` 11 | fragment pokemonFields on Pokemon { 12 | name 13 | weight { 14 | minimum 15 | } 16 | } 17 | `); 18 | 19 | interface Props { 20 | data: (FragmentOf & FragmentOf) | null; 21 | } 22 | 23 | export const Pokemon = ({ data }: Props) => { 24 | const pokemon = readFragment(PokemonFields, data); 25 | const resistant = readFragment(Fields.Pokemon, data); 26 | if (!pokemon || !resistant) { 27 | return null; 28 | } 29 | 30 | return ( 31 |
  • 32 | {pokemon.name} 33 | {resistant.resistant} 34 |
  • 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/example-tada/src/graphql.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | import type { introspection } from '../introspection.d.ts'; 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: introspection; 6 | }>(); 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 | export { readFragment } from 'gql.tada'; 10 | -------------------------------------------------------------------------------- /packages/example-tada/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | import { graphql } from './graphql'; 3 | import { Fields, Pokemon, PokemonFields } from './Pokemon'; 4 | 5 | const PokemonQuery = graphql(` 6 | query Po($id: ID!) { 7 | pokemon(id: $id) { 8 | id 9 | fleeRate 10 | ...Pok 11 | ...pokemonFields 12 | attacks { 13 | special { 14 | name 15 | damage 16 | } 17 | } 18 | weight { 19 | minimum 20 | maximum 21 | } 22 | name 23 | __typename 24 | } 25 | pokemons { 26 | name 27 | maxCP 28 | maxHP 29 | types 30 | fleeRate 31 | } 32 | } 33 | `, [PokemonFields, Fields.Pokemon]) 34 | 35 | const persisted = graphql.persisted("sha256:78c769ed6cfef67e17e579a2abfe4da27bd51e09ed832a88393148bcce4c5a7d") 36 | 37 | const Pokemons = () => { 38 | const [result] = useQuery({ 39 | query: PokemonQuery, 40 | variables: { id: '' } 41 | }); 42 | 43 | // @ts-expect-error 44 | const [sel] = result.data?.pokemons; 45 | console.log(sel.fleeRate) 46 | const selected = result.data?.pokemons?.at(0)! 47 | console.log(result.data?.pokemons?.at(0)?.maxCP) 48 | console.log(selected.maxHP) 49 | const names = result.data?.pokemons?.map(x => x?.name) 50 | console.log(names) 51 | const pos = result.data?.pokemons?.map(x => ({ ...x })) 52 | console.log(pos && pos[0].types) 53 | 54 | // Works 55 | console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name) 56 | 57 | // Works 58 | const { fleeRate } = result.data?.pokemon || {}; 59 | console.log(fleeRate) 60 | // Works 61 | const po = result.data?.pokemon; 62 | 63 | // @ts-expect-error 64 | const { pokemon: { weight: { minimum } } } = result.data || {}; 65 | console.log(po?.name, minimum) 66 | 67 | // Works 68 | const { pokemon } = result.data || {}; 69 | console.log(pokemon?.weight?.maximum) 70 | 71 | // @ts-ignore 72 | return ; 73 | } 74 | -------------------------------------------------------------------------------- /packages/example-tada/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schemas": [ 7 | { 8 | "name": "pokemons", 9 | "schema": "./schema.graphql", 10 | "tadaOutputLocation": "./introspection.d.ts" 11 | } 12 | ] 13 | } 14 | ], 15 | "jsx": "react-jsx", 16 | /* Language and Environment */ 17 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 18 | /* Modules */ 19 | "module": "commonjs" /* Specify what module code is generated. */, 20 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 21 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 22 | /* Type Checking */ 23 | "strict": true /* Enable all strict type-checking options. */, 24 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/example/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/example/__generated__/baseGraphQLSP.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { 4 | [K in keyof T]: T[K]; 5 | }; 6 | export type MakeOptional = Omit & { 7 | [SubKey in K]?: Maybe; 8 | }; 9 | export type MakeMaybe = Omit & { 10 | [SubKey in K]: Maybe; 11 | }; 12 | export type MakeEmpty< 13 | T extends { [key: string]: unknown }, 14 | K extends keyof T 15 | > = { [_ in K]?: never }; 16 | export type Incremental = 17 | | T 18 | | { 19 | [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; 20 | }; 21 | /** All built-in and custom scalars, mapped to their actual values */ 22 | export type Scalars = { 23 | ID: { input: string; output: string }; 24 | String: { input: string; output: string }; 25 | Boolean: { input: boolean; output: boolean }; 26 | Int: { input: number; output: number }; 27 | Float: { input: number; output: number }; 28 | }; 29 | 30 | /** Move a Pokémon can perform with the associated damage and type. */ 31 | export type Attack = { 32 | __typename: 'Attack'; 33 | damage?: Maybe; 34 | name?: Maybe; 35 | type?: Maybe; 36 | }; 37 | 38 | export type AttacksConnection = { 39 | __typename: 'AttacksConnection'; 40 | fast?: Maybe>>; 41 | special?: Maybe>>; 42 | }; 43 | 44 | /** Requirement that prevents an evolution through regular means of levelling up. */ 45 | export type EvolutionRequirement = { 46 | __typename: 'EvolutionRequirement'; 47 | amount?: Maybe; 48 | name?: Maybe; 49 | }; 50 | 51 | export type Pokemon = { 52 | __typename: 'Pokemon'; 53 | attacks?: Maybe; 54 | /** @deprecated And this is the reason why */ 55 | classification?: Maybe; 56 | evolutionRequirements?: Maybe>>; 57 | evolutions?: Maybe>>; 58 | /** Likelihood of an attempt to catch a Pokémon to fail. */ 59 | fleeRate?: Maybe; 60 | height?: Maybe; 61 | id: Scalars['ID']['output']; 62 | /** Maximum combat power a Pokémon may achieve at max level. */ 63 | maxCP?: Maybe; 64 | /** Maximum health points a Pokémon may achieve at max level. */ 65 | maxHP?: Maybe; 66 | name: Scalars['String']['output']; 67 | resistant?: Maybe>>; 68 | types?: Maybe>>; 69 | weaknesses?: Maybe>>; 70 | weight?: Maybe; 71 | }; 72 | 73 | export type PokemonDimension = { 74 | __typename: 'PokemonDimension'; 75 | maximum?: Maybe; 76 | minimum?: Maybe; 77 | }; 78 | 79 | /** Elemental property associated with either a Pokémon or one of their moves. */ 80 | export type PokemonType = 81 | | 'Bug' 82 | | 'Dark' 83 | | 'Dragon' 84 | | 'Electric' 85 | | 'Fairy' 86 | | 'Fighting' 87 | | 'Fire' 88 | | 'Flying' 89 | | 'Ghost' 90 | | 'Grass' 91 | | 'Ground' 92 | | 'Ice' 93 | | 'Normal' 94 | | 'Poison' 95 | | 'Psychic' 96 | | 'Rock' 97 | | 'Steel' 98 | | 'Water'; 99 | 100 | export type Query = { 101 | __typename: 'Query'; 102 | /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 103 | pokemon?: Maybe; 104 | /** List out all Pokémon, optionally in pages */ 105 | pokemons?: Maybe>>; 106 | }; 107 | 108 | export type QueryPokemonArgs = { 109 | id: Scalars['ID']['input']; 110 | }; 111 | 112 | export type QueryPokemonsArgs = { 113 | limit?: InputMaybe; 114 | skip?: InputMaybe; 115 | }; 116 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@urql/core": "^3.0.0", 14 | "graphql": "^16.8.1", 15 | "@graphql-typed-document-node/core": "^3.2.0" 16 | }, 17 | "devDependencies": { 18 | "typescript": "^5.3.3", 19 | "@0no-co/graphqlsp": "file:../graphqlsp" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/example/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /packages/example/src/Pokemon.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from "../__generated__/baseGraphQLSP" 2 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 | export type PokemonFieldsFragment = { __typename: 'Pokemon', id: string, name: string, attacks?: { __typename: 'AttacksConnection', fast?: Array<{ __typename: 'Attack', damage?: number | null, name?: string | null } | null> | null } | null }; 4 | 5 | export type WeaknessFieldsFragment = { __typename: 'Pokemon', weaknesses?: Array | null }; 6 | 7 | export const PokemonFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"pokemonFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Pokemon"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"attacks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fast"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"damage"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; 8 | export const WeaknessFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"weaknessFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Pokemon"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weaknesses"}}]}}]} as unknown as DocumentNode; -------------------------------------------------------------------------------- /packages/example/src/Pokemon.ts: -------------------------------------------------------------------------------- 1 | import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 2 | import { createClient, gql } from '@urql/core'; 3 | 4 | export const PokemonFields = gql` 5 | fragment pokemonFields on Pokemon { 6 | id 7 | name 8 | attacks { 9 | fast { 10 | damage 11 | name 12 | } 13 | } 14 | } 15 | ` as typeof import('./Pokemon.generated').PokemonFieldsFragmentDoc; 16 | 17 | export const WeakFields = gql` 18 | fragment weaknessFields on Pokemon { 19 | weaknesses 20 | } 21 | ` as typeof import('./Pokemon.generated').WeaknessFieldsFragmentDoc; 22 | 23 | export const Pokemon = (data: any) => { 24 | const pokemon = useFragment(PokemonFields, data); 25 | return `hi ${pokemon.name}`; 26 | }; 27 | 28 | type X = { hello: string }; 29 | 30 | const x: X = { hello: '' }; 31 | 32 | export function useFragment( 33 | _fragment: TypedDocumentNode, 34 | data: any 35 | ): Type { 36 | return data; 37 | } 38 | -------------------------------------------------------------------------------- /packages/example/src/index.generated.ts: -------------------------------------------------------------------------------- 1 | import * as Types from '../__generated__/baseGraphQLSP'; 2 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 | export type PoQueryVariables = Types.Exact<{ 4 | id: Types.Scalars['ID']['input']; 5 | }>; 6 | 7 | export type PoQuery = { 8 | __typename: 'Query'; 9 | pokemon?: { 10 | __typename: 'Pokemon'; 11 | id: string; 12 | fleeRate?: number | null; 13 | } | null; 14 | }; 15 | 16 | export const PoDocument = { 17 | kind: 'Document', 18 | definitions: [ 19 | { 20 | kind: 'OperationDefinition', 21 | operation: 'query', 22 | name: { kind: 'Name', value: 'Po' }, 23 | variableDefinitions: [ 24 | { 25 | kind: 'VariableDefinition', 26 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, 27 | type: { 28 | kind: 'NonNullType', 29 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } }, 30 | }, 31 | }, 32 | ], 33 | selectionSet: { 34 | kind: 'SelectionSet', 35 | selections: [ 36 | { 37 | kind: 'Field', 38 | name: { kind: 'Name', value: 'pokemon' }, 39 | arguments: [ 40 | { 41 | kind: 'Argument', 42 | name: { kind: 'Name', value: 'id' }, 43 | value: { 44 | kind: 'Variable', 45 | name: { kind: 'Name', value: 'id' }, 46 | }, 47 | }, 48 | ], 49 | selectionSet: { 50 | kind: 'SelectionSet', 51 | selections: [ 52 | { kind: 'Field', name: { kind: 'Name', value: 'id' } }, 53 | { kind: 'Field', name: { kind: 'Name', value: 'fleeRate' } }, 54 | { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, 55 | ], 56 | }, 57 | }, 58 | ], 59 | }, 60 | }, 61 | ], 62 | } as unknown as DocumentNode; 63 | -------------------------------------------------------------------------------- /packages/example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { gql, createClient } from '@urql/core'; 2 | import { Pokemon, PokemonFields, WeakFields } from './Pokemon'; 3 | 4 | const PokemonQuery = gql` 5 | query Po($id: ID!) { 6 | pokemon(id: $id) { 7 | id 8 | fleeRate 9 | __typename 10 | } 11 | } 12 | ` as typeof import('./index.generated').PoDocument; 13 | 14 | client 15 | .query(PokemonQuery, { id: '' }) 16 | .toPromise() 17 | .then(result => { 18 | result.data?.pokemons; 19 | }); 20 | -------------------------------------------------------------------------------- /packages/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schema": "./schema.graphql", 7 | "templateIsCallExpression": false 8 | } 9 | ], 10 | /* Language and Environment */ 11 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 12 | /* Modules */ 13 | "module": "commonjs" /* Specify what module code is generated. */, 14 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 15 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 16 | /* Type Checking */ 17 | "strict": true /* Enable all strict type-checking options. */, 18 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/graphqlsp/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | node_modules 4 | scripts 5 | example 6 | .vscode 7 | .husky 8 | .github 9 | .changeset 10 | pnpm-lock.yaml 11 | tsconfig.json 12 | -------------------------------------------------------------------------------- /packages/graphqlsp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 0no.co 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/graphqlsp/README.md: -------------------------------------------------------------------------------- 1 | # GraphQLSP 2 | 3 | This is a TypeScript LSP Plugin that will recognise documents in your 4 | TypeScript code and help you out with hover-information, diagnostics and 5 | auto-complete. 6 | 7 | ## Features 8 | 9 | - Hover information showing the decriptions of fields 10 | - Diagnostics for adding fields that don't exist, are deprecated, missmatched argument types, ... 11 | - Auto-complete inside your editor for fields 12 | - Will warn you when you are importing from a file that is exporting fragments that you're not using 13 | 14 | > Note that this plugin does not do syntax highlighting, for that you still need something like 15 | > [the VSCode/... plugin](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax) 16 | 17 | ## Installation 18 | 19 | ```sh 20 | npm install -D @0no-co/graphqlsp 21 | ``` 22 | 23 | ## Usage 24 | 25 | Go to your `tsconfig.json` and add 26 | 27 | ```json 28 | { 29 | "compilerOptions": { 30 | "plugins": [ 31 | { 32 | "name": "@0no-co/graphqlsp", 33 | "schema": "./schema.graphql" 34 | } 35 | ] 36 | } 37 | } 38 | ``` 39 | 40 | now restart your TS-server and you should be good to go, ensure you are using the 41 | workspace version of TypeScript. In VSCode you can do so by clicking the bottom right 42 | when on a TypeScript file or adding a file like [this](https://github.com/0no-co/GraphQLSP/blob/main/packages/example/.vscode/settings.json). 43 | 44 | > If you are using VSCode ensure that your editor is using [the Workspace Version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript) 45 | > this can be done by manually selecting it or adding a `.vscode/config.json` with the contents of 46 | > 47 | > ```json 48 | > { 49 | > "typescript.tsdk": "node_modules/typescript/lib", 50 | > "typescript.enablePromptUseWorkspaceTsdk": true 51 | > } 52 | > ``` 53 | 54 | ### Configuration 55 | 56 | **Required** 57 | 58 | - `schema` allows you to specify a url, `.json` or `.graphql` file as your schema. If you need to specify headers for your introspection 59 | you can opt into the object notation i.e. `{ "schema": { "url": "x", "headers": { "Authorization": "y" } }}` 60 | 61 | **Optional** 62 | 63 | - `template` add an additional template to the defaults `gql` and `graphql` 64 | - `templateIsCallExpression` this tells our client that you are using `graphql('doc')` (default: true) 65 | when using `false` it will look for tagged template literals 66 | - `shouldCheckForColocatedFragments` when turned on, this will scan your imports to find 67 | unused fragments and provide a message notifying you about them (only works with call-expressions, default: true) 68 | - `trackFieldUsage` this only works with the client-preset, when turned on it will warn you about 69 | unused fields within the same file. (only works with call-expressions, default: true) 70 | - `tadaOutputLocation` when using `gql.tada` this can be convenient as it automatically generates 71 | an `introspection.ts` file for you, just give it the directory to output to and you're done 72 | - `reservedKeys` this setting will affect `trackFieldUsage`, you can enter keys here that will be ignored 73 | from usage tracking, so when they are unused in the component but used in i.e. the normalised cache you 74 | won't get annoying warnings. (default `id`, `_id` and `__typename`, example: ['slug']) 75 | - `tadaDisablePreprocessing` this setting disables the optimisation of `tadaOutput` to a pre-processed TypeScript type, this is off by default. 76 | 77 | ## Tracking unused fields 78 | 79 | Currently the tracking unused fields feature has a few caveats with regards to tracking, first and foremost 80 | it will only track the result and the accessed properties in the same file to encourage 81 | [fragment co-location](https://www.apollographql.com/docs/react/data/fragments/#colocating-fragments). 82 | 83 | Secondly, we don't track mutations/subscriptions as some folks will add additional fields to properly support 84 | normalised cache updates. 85 | 86 | ## Fragment masking 87 | 88 | When we use a `useQuery` that supports `TypedDocumentNode` it will automatically pick up the typings 89 | from the `query` you provide it. However for fragments this could become a bit more troublesome, the 90 | minimal way of providing typings for a fragment would be the following: 91 | 92 | ```tsx 93 | import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 94 | 95 | export const PokemonFields = gql` 96 | fragment pokemonFields on Pokemon { 97 | id 98 | name 99 | } 100 | ` as typeof import('./Pokemon.generated').PokemonFieldsFragmentDoc; 101 | 102 | export const Pokemon = props => { 103 | const pokemon = useFragment(props.pokemon, PokemonFields); 104 | }; 105 | 106 | export function useFragment( 107 | data: any, 108 | _fragment: TypedDocumentNode 109 | ): Type { 110 | return data; 111 | } 112 | ``` 113 | 114 | This is mainly needed in cases where this isn't supported out of the box and mainly serves as a way 115 | for you to case your types. 116 | 117 | ## 💙 [Sponsors](https://github.com/sponsors/urql-graphql) 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
    BigCommerce
    BigCommerce
    WunderGraph
    WunderGraph
    The Guild
    The Guild
    126 | 127 | 128 | 129 | 130 | 131 |
    BeatGig
    BeatGig
    132 | 133 | ## Local development 134 | 135 | Run `pnpm i` at the root. Open `packages/example` by running `code packages/example` or if you want to leverage 136 | breakpoints do it with the `TSS_DEBUG_BRK=9559` prefix. When you make changes in `packages/graphqlsp` all you need 137 | to do is run `pnpm i` in your other editor and restart the `TypeScript server` for the changes to apply. 138 | 139 | > Ensure that both instances of your editor are using the Workspace Version of TypeScript 140 | -------------------------------------------------------------------------------- /packages/graphqlsp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@0no-co/graphqlsp", 3 | "version": "1.12.16", 4 | "description": "TypeScript LSP plugin that finds GraphQL documents in your code and provides hints and auto-generates types.", 5 | "main": "./dist/graphqlsp", 6 | "module": "./dist/graphqlsp.mjs", 7 | "types": "./dist/graphqlsp.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/graphqlsp.d.ts", 11 | "import": "./dist/graphqlsp.mjs", 12 | "require": "./dist/graphqlsp.js", 13 | "source": "./src/index.ts" 14 | }, 15 | "./api": { 16 | "types": "./dist/api.d.ts", 17 | "import": "./dist/api.mjs", 18 | "require": "./dist/api.js", 19 | "source": "./src/api.ts" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "scripts": { 24 | "build": "rollup -c ../../scripts/rollup.config.mjs", 25 | "dev": "NODE_ENV=development pnpm build --watch", 26 | "prepublishOnly": "pnpm build" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/0no-co/GraphQLSP.git" 31 | }, 32 | "keywords": [ 33 | "GraphQL", 34 | "TypeScript", 35 | "LSP", 36 | "Typed-document-node" 37 | ], 38 | "author": "0no.co ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/0no-co/GraphQLSP/issues" 42 | }, 43 | "homepage": "https://github.com/0no-co/GraphQLSP#readme", 44 | "devDependencies": { 45 | "@0no-co/graphql.web": "^1.0.4", 46 | "@sindresorhus/fnv1a": "^2.0.0", 47 | "@types/node": "^18.15.11", 48 | "graphql-language-service": "^5.2.0", 49 | "lru-cache": "^10.0.1", 50 | "typescript": "^5.3.3" 51 | }, 52 | "dependencies": { 53 | "@gql.tada/internal": "^1.0.0", 54 | "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" 55 | }, 56 | "peerDependencies": { 57 | "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", 58 | "typescript": "^5.0.0" 59 | }, 60 | "publishConfig": { 61 | "provenance": true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/api.ts: -------------------------------------------------------------------------------- 1 | export { getGraphQLDiagnostics } from './diagnostics'; 2 | export { init } from './ts'; 3 | 4 | export { 5 | findAllPersistedCallExpressions, 6 | findAllCallExpressions, 7 | unrollTadaFragments, 8 | } from './ast'; 9 | 10 | export { 11 | getDocumentReferenceFromTypeQuery, 12 | getDocumentReferenceFromDocumentNode, 13 | } from './persisted'; 14 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ast/checks.ts: -------------------------------------------------------------------------------- 1 | import { ts } from '../ts'; 2 | import { templates } from './templates'; 3 | 4 | /** Checks for an immediately-invoked function expression */ 5 | export const isIIFE = (node: ts.Node): boolean => 6 | ts.isCallExpression(node) && 7 | node.arguments.length === 0 && 8 | (ts.isFunctionExpression(node.expression) || 9 | ts.isArrowFunction(node.expression)) && 10 | !node.expression.asteriskToken && 11 | !node.expression.modifiers?.length; 12 | 13 | /** Checks if node is a known identifier of graphql functions ('graphql' or 'gql') */ 14 | export const isGraphQLFunctionIdentifier = ( 15 | node: ts.Node 16 | ): node is ts.Identifier => 17 | ts.isIdentifier(node) && templates.has(node.escapedText as string); 18 | 19 | /** If `checker` is passed, checks if node (as identifier/expression) is a gql.tada graphql() function */ 20 | export const isTadaGraphQLFunction = ( 21 | node: ts.Node, 22 | checker: ts.TypeChecker | undefined 23 | ): node is ts.LeftHandSideExpression => { 24 | if (!ts.isLeftHandSideExpression(node)) return false; 25 | const type = checker?.getTypeAtLocation(node); 26 | // Any function that has both a `scalar` and `persisted` property 27 | // is automatically considered a gql.tada graphql() function. 28 | return ( 29 | type != null && 30 | type.getProperty('scalar') != null && 31 | type.getProperty('persisted') != null 32 | ); 33 | }; 34 | 35 | /** If `checker` is passed, checks if node is a gql.tada graphql() call */ 36 | export const isTadaGraphQLCall = ( 37 | node: ts.CallExpression, 38 | checker: ts.TypeChecker | undefined 39 | ): boolean => { 40 | // We expect graphql() to be called with either a string literal 41 | // or a string literal and an array of fragments 42 | if (!ts.isCallExpression(node)) { 43 | return false; 44 | } else if (node.arguments.length < 1 || node.arguments.length > 2) { 45 | return false; 46 | } else if (!ts.isStringLiteralLike(node.arguments[0]!)) { 47 | return false; 48 | } 49 | return checker ? isTadaGraphQLFunction(node.expression, checker) : false; 50 | }; 51 | 52 | /** Checks if node is a gql.tada graphql.persisted() call */ 53 | export const isTadaPersistedCall = ( 54 | node: ts.Node | undefined, 55 | checker: ts.TypeChecker | undefined 56 | ): node is ts.CallExpression => { 57 | if (!node) { 58 | return false; 59 | } else if (!ts.isCallExpression(node)) { 60 | return false; 61 | } else if (!ts.isPropertyAccessExpression(node.expression)) { 62 | return false; // rejecting non property access calls: .() 63 | } else if ( 64 | !ts.isIdentifier(node.expression.name) || 65 | node.expression.name.escapedText !== 'persisted' 66 | ) { 67 | return false; // rejecting calls on anyting but 'persisted': .persisted() 68 | } else if (isGraphQLFunctionIdentifier(node.expression.expression)) { 69 | return true; 70 | } else { 71 | return isTadaGraphQLFunction(node.expression.expression, checker); 72 | } 73 | }; 74 | 75 | // As per check in `isGraphQLCall()` below, enforces arguments length 76 | export type GraphQLCallNode = ts.CallExpression & { 77 | arguments: [ts.Expression] | [ts.Expression, ts.Expression]; 78 | }; 79 | 80 | /** Checks if node is a gql.tada or regular graphql() call */ 81 | export const isGraphQLCall = ( 82 | node: ts.Node, 83 | checker: ts.TypeChecker | undefined 84 | ): node is GraphQLCallNode => { 85 | return ( 86 | ts.isCallExpression(node) && 87 | node.arguments.length >= 1 && 88 | node.arguments.length <= 2 && 89 | (isGraphQLFunctionIdentifier(node.expression) || 90 | isTadaGraphQLCall(node, checker)) 91 | ); 92 | }; 93 | 94 | /** Checks if node is a gql/graphql tagged template literal */ 95 | export const isGraphQLTag = ( 96 | node: ts.Node 97 | ): node is ts.TaggedTemplateExpression => 98 | ts.isTaggedTemplateExpression(node) && isGraphQLFunctionIdentifier(node.tag); 99 | 100 | /** Retrieves the `__name` branded tag from gql.tada `graphql()` or `graphql.persisted()` calls */ 101 | export const getSchemaName = ( 102 | node: ts.CallExpression, 103 | typeChecker: ts.TypeChecker | undefined, 104 | isTadaPersistedCall = false 105 | ): string | null => { 106 | if (!typeChecker) return null; 107 | const type = typeChecker.getTypeAtLocation( 108 | // When calling `graphql.persisted`, we need to access the `graphql` part of 109 | // the expression; `node.expression` is the `.persisted` part 110 | isTadaPersistedCall ? node.getChildAt(0).getChildAt(0) : node.expression 111 | ); 112 | if (type) { 113 | const brandTypeSymbol = type.getProperty('__name'); 114 | if (brandTypeSymbol) { 115 | const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol); 116 | if (brand.isUnionOrIntersection()) { 117 | const found = brand.types.find(x => x.isStringLiteral()); 118 | return found && found.isStringLiteral() ? found.value : null; 119 | } else if (brand.isStringLiteral()) { 120 | return brand.value; 121 | } 122 | } 123 | } 124 | return null; 125 | }; 126 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ast/cursor.ts: -------------------------------------------------------------------------------- 1 | import { IPosition } from 'graphql-language-service'; 2 | 3 | export class Cursor implements IPosition { 4 | line: number; 5 | character: number; 6 | 7 | constructor(line: number, char: number) { 8 | this.line = line; 9 | this.character = char; 10 | } 11 | 12 | setLine(line: number) { 13 | this.line = line; 14 | } 15 | 16 | setCharacter(character: number) { 17 | this.character = character; 18 | } 19 | 20 | lessThanOrEqualTo(position: IPosition) { 21 | return ( 22 | this.line < position.line || 23 | (this.line === position.line && this.character <= position.character) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ast/resolve.ts: -------------------------------------------------------------------------------- 1 | import { print } from '@0no-co/graphql.web'; 2 | import { ts } from '../ts'; 3 | import { findNode } from '.'; 4 | import { getSource } from '../ast'; 5 | 6 | type TemplateResult = { 7 | combinedText: string; 8 | resolvedSpans: Array<{ 9 | lines: number; 10 | identifier: string; 11 | original: { start: number; length: number }; 12 | new: { start: number; length: number }; 13 | }>; 14 | }; 15 | 16 | export function resolveTemplate( 17 | node: ts.TaggedTemplateExpression | ts.StringLiteralLike, 18 | filename: string, 19 | info: ts.server.PluginCreateInfo 20 | ): TemplateResult { 21 | if (ts.isStringLiteralLike(node)) { 22 | return { combinedText: node.getText().slice(1, -1), resolvedSpans: [] }; 23 | } 24 | 25 | let templateText = node.template.getText().slice(1, -1); 26 | if ( 27 | ts.isNoSubstitutionTemplateLiteral(node.template) || 28 | node.template.templateSpans.length === 0 29 | ) { 30 | return { combinedText: templateText, resolvedSpans: [] }; 31 | } 32 | 33 | let addedCharacters = 0; 34 | const resolvedSpans = node.template.templateSpans 35 | .map(span => { 36 | if (ts.isIdentifier(span.expression)) { 37 | const definitions = info.languageService.getDefinitionAtPosition( 38 | filename, 39 | span.expression.getStart() 40 | ); 41 | 42 | const def = definitions && definitions[0]; 43 | if (!def) return; 44 | 45 | const src = getSource(info, def.fileName); 46 | if (!src) return; 47 | 48 | const node = findNode(src, def.textSpan.start); 49 | if (!node || !node.parent) return; 50 | 51 | const parent = node.parent; 52 | if (ts.isVariableDeclaration(parent)) { 53 | const identifierName = span.expression.escapedText; 54 | // we reduce by two to account for the "${" 55 | const originalStart = span.expression.getStart() - 2; 56 | const originalRange = { 57 | start: originalStart, 58 | // we add 1 to account for the "}" 59 | length: span.expression.end - originalStart + 1, 60 | }; 61 | if ( 62 | parent.initializer && 63 | ts.isTaggedTemplateExpression(parent.initializer) 64 | ) { 65 | const text = resolveTemplate( 66 | parent.initializer, 67 | def.fileName, 68 | info 69 | ); 70 | templateText = templateText.replace( 71 | '${' + span.expression.escapedText + '}', 72 | text.combinedText 73 | ); 74 | 75 | const alteredSpan = { 76 | lines: text.combinedText.split('\n').length, 77 | identifier: identifierName, 78 | original: originalRange, 79 | new: { 80 | start: originalRange.start + addedCharacters, 81 | length: text.combinedText.length, 82 | }, 83 | }; 84 | addedCharacters += text.combinedText.length - originalRange.length; 85 | return alteredSpan; 86 | } else if ( 87 | parent.initializer && 88 | ts.isAsExpression(parent.initializer) && 89 | ts.isTaggedTemplateExpression(parent.initializer.expression) 90 | ) { 91 | const text = resolveTemplate( 92 | parent.initializer.expression, 93 | def.fileName, 94 | info 95 | ); 96 | templateText = templateText.replace( 97 | '${' + span.expression.escapedText + '}', 98 | text.combinedText 99 | ); 100 | const alteredSpan = { 101 | lines: text.combinedText.split('\n').length, 102 | identifier: identifierName, 103 | original: originalRange, 104 | new: { 105 | start: originalRange.start + addedCharacters, 106 | length: text.combinedText.length, 107 | }, 108 | }; 109 | addedCharacters += text.combinedText.length - originalRange.length; 110 | return alteredSpan; 111 | } else if ( 112 | parent.initializer && 113 | ts.isAsExpression(parent.initializer) && 114 | ts.isAsExpression(parent.initializer.expression) && 115 | ts.isObjectLiteralExpression( 116 | parent.initializer.expression.expression 117 | ) 118 | ) { 119 | const astObject = JSON.parse( 120 | parent.initializer.expression.expression.getText() 121 | ); 122 | const resolvedTemplate = print(astObject); 123 | templateText = templateText.replace( 124 | '${' + span.expression.escapedText + '}', 125 | resolvedTemplate 126 | ); 127 | const alteredSpan = { 128 | lines: resolvedTemplate.split('\n').length, 129 | identifier: identifierName, 130 | original: originalRange, 131 | new: { 132 | start: originalRange.start + addedCharacters, 133 | length: resolvedTemplate.length, 134 | }, 135 | }; 136 | addedCharacters += resolvedTemplate.length - originalRange.length; 137 | return alteredSpan; 138 | } 139 | 140 | return undefined; 141 | } 142 | } 143 | 144 | return undefined; 145 | }) 146 | .filter(Boolean) as TemplateResult['resolvedSpans']; 147 | 148 | return { combinedText: templateText, resolvedSpans }; 149 | } 150 | 151 | export const resolveTadaFragmentArray = ( 152 | node: ts.Expression | undefined 153 | ): undefined | readonly ts.Identifier[] => { 154 | if (!node) return undefined; 155 | // NOTE: Remove `as T`, users may commonly use `as const` for no reason 156 | while (ts.isAsExpression(node)) node = node.expression; 157 | if (!ts.isArrayLiteralExpression(node)) return undefined; 158 | // NOTE: Let's avoid the allocation of another array here if we can 159 | if (node.elements.every(ts.isIdentifier)) return node.elements; 160 | const identifiers: ts.Identifier[] = []; 161 | for (let element of node.elements) { 162 | while (ts.isPropertyAccessExpression(element)) element = element.name; 163 | if (ts.isIdentifier(element)) identifiers.push(element); 164 | } 165 | return identifiers; 166 | }; 167 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ast/templates.ts: -------------------------------------------------------------------------------- 1 | export const templates = new Set(['gql', 'graphql']); 2 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ast/token.ts: -------------------------------------------------------------------------------- 1 | import { ts } from '../ts'; 2 | import { onlineParser, State, CharacterStream } from 'graphql-language-service'; 3 | 4 | export interface Token { 5 | start: number; 6 | end: number; 7 | string: string; 8 | tokenKind: string; 9 | line: number; 10 | state: State; 11 | } 12 | 13 | export const getToken = ( 14 | template: ts.Expression, 15 | cursorPosition: number 16 | ): Token | undefined => { 17 | if (!ts.isTemplateLiteral(template) && !ts.isStringLiteralLike(template)) { 18 | return undefined; 19 | } 20 | 21 | const text = template.getText().slice(1, -1); 22 | const input = text.split('\n'); 23 | const parser = onlineParser(); 24 | const state = parser.startState(); 25 | let cPos = template.getStart() + 1; 26 | 27 | let foundToken: Token | undefined = undefined; 28 | let prevToken: Token | undefined = undefined; 29 | for (let line = 0; line < input.length; line++) { 30 | if (foundToken) continue; 31 | const lPos = cPos - 1; 32 | const stream = new CharacterStream(input[line] + '\n'); 33 | while (!stream.eol()) { 34 | const token = parser.token(stream, state); 35 | const string = stream.current(); 36 | 37 | if ( 38 | lPos + stream.getStartOfToken() + 1 <= cursorPosition && 39 | lPos + stream.getCurrentPosition() >= cursorPosition 40 | ) { 41 | foundToken = prevToken 42 | ? prevToken 43 | : { 44 | line, 45 | start: stream.getStartOfToken() + 1, 46 | end: stream.getCurrentPosition(), 47 | string, 48 | state, 49 | tokenKind: token, 50 | }; 51 | break; 52 | } else if (string === 'on') { 53 | prevToken = { 54 | line, 55 | start: stream.getStartOfToken() + 1, 56 | end: stream.getCurrentPosition(), 57 | string, 58 | state, 59 | tokenKind: token, 60 | }; 61 | } else if (string === '.' || string === '..') { 62 | prevToken = { 63 | line, 64 | start: stream.getStartOfToken() + 1, 65 | end: stream.getCurrentPosition(), 66 | string, 67 | state, 68 | tokenKind: token, 69 | }; 70 | } else { 71 | prevToken = undefined; 72 | } 73 | } 74 | 75 | cPos += input[line]!.length + 1; 76 | } 77 | 78 | return foundToken; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/checkImports.ts: -------------------------------------------------------------------------------- 1 | import { ts } from './ts'; 2 | import { FragmentDefinitionNode, Kind, parse } from 'graphql'; 3 | 4 | import { findAllCallExpressions, findAllImports, getSource } from './ast'; 5 | import { resolveTemplate } from './ast/resolve'; 6 | import { 7 | VariableDeclaration, 8 | VariableStatement, 9 | isSourceFile, 10 | } from 'typescript'; 11 | 12 | export const MISSING_FRAGMENT_CODE = 52003; 13 | 14 | export const getColocatedFragmentNames = ( 15 | source: ts.SourceFile, 16 | info: ts.server.PluginCreateInfo 17 | ): Record< 18 | string, 19 | { start: number; length: number; fragments: Array } 20 | > => { 21 | const imports = findAllImports(source); 22 | const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 23 | 24 | const importSpecifierToFragments: Record< 25 | string, 26 | { start: number; length: number; fragments: Array } 27 | > = {}; 28 | 29 | if (!typeChecker) return importSpecifierToFragments; 30 | 31 | if (imports.length) { 32 | imports.forEach(imp => { 33 | if (!imp.importClause) return; 34 | 35 | if (imp.importClause.name) { 36 | const definitions = info.languageService.getDefinitionAtPosition( 37 | source.fileName, 38 | imp.importClause.name.getStart() 39 | ); 40 | const def = definitions && definitions[0]; 41 | if (def) { 42 | if (def.fileName.includes('node_modules')) return; 43 | 44 | const externalSource = getSource(info, def.fileName); 45 | if (!externalSource) return; 46 | 47 | const fragmentsForImport = getFragmentsInSource( 48 | externalSource, 49 | typeChecker, 50 | info 51 | ); 52 | 53 | const names = fragmentsForImport.map(fragment => fragment.name.value); 54 | const key = imp.moduleSpecifier.getText(); 55 | let fragmentsEntry = importSpecifierToFragments[key]; 56 | if (names.length && fragmentsEntry) { 57 | fragmentsEntry.fragments = fragmentsEntry.fragments.concat(names); 58 | } else if (names.length && !fragmentsEntry) { 59 | importSpecifierToFragments[key] = fragmentsEntry = { 60 | start: imp.moduleSpecifier.getStart(), 61 | length: imp.moduleSpecifier.getText().length, 62 | fragments: names, 63 | }; 64 | } 65 | } 66 | } 67 | 68 | if ( 69 | imp.importClause.namedBindings && 70 | ts.isNamespaceImport(imp.importClause.namedBindings) 71 | ) { 72 | const definitions = info.languageService.getDefinitionAtPosition( 73 | source.fileName, 74 | imp.importClause.namedBindings.getStart() 75 | ); 76 | const def = definitions && definitions[0]; 77 | if (def) { 78 | if (def.fileName.includes('node_modules')) return; 79 | 80 | const externalSource = getSource(info, def.fileName); 81 | if (!externalSource) return; 82 | 83 | const fragmentsForImport = getFragmentsInSource( 84 | externalSource, 85 | typeChecker, 86 | info 87 | ); 88 | const names = fragmentsForImport.map(fragment => fragment.name.value); 89 | const key = imp.moduleSpecifier.getText(); 90 | let fragmentsEntry = importSpecifierToFragments[key]; 91 | if (names.length && fragmentsEntry) { 92 | fragmentsEntry.fragments = fragmentsEntry.fragments.concat(names); 93 | } else if (names.length && !fragmentsEntry) { 94 | importSpecifierToFragments[key] = fragmentsEntry = { 95 | start: imp.moduleSpecifier.getStart(), 96 | length: imp.moduleSpecifier.getText().length, 97 | fragments: names, 98 | }; 99 | } 100 | } 101 | } else if ( 102 | imp.importClause.namedBindings && 103 | ts.isNamedImportBindings(imp.importClause.namedBindings) 104 | ) { 105 | imp.importClause.namedBindings.elements.forEach(el => { 106 | const definitions = info.languageService.getDefinitionAtPosition( 107 | source.fileName, 108 | el.getStart() 109 | ); 110 | const def = definitions && definitions[0]; 111 | if (def) { 112 | if (def.fileName.includes('node_modules')) return; 113 | 114 | const externalSource = getSource(info, def.fileName); 115 | if (!externalSource) return; 116 | 117 | const fragmentsForImport = getFragmentsInSource( 118 | externalSource, 119 | typeChecker, 120 | info 121 | ); 122 | const names = fragmentsForImport.map( 123 | fragment => fragment.name.value 124 | ); 125 | const key = imp.moduleSpecifier.getText(); 126 | let fragmentsEntry = importSpecifierToFragments[key]; 127 | if (names.length && fragmentsEntry) { 128 | fragmentsEntry.fragments = fragmentsEntry.fragments.concat(names); 129 | } else if (names.length && !fragmentsEntry) { 130 | importSpecifierToFragments[key] = fragmentsEntry = { 131 | start: imp.moduleSpecifier.getStart(), 132 | length: imp.moduleSpecifier.getText().length, 133 | fragments: names, 134 | }; 135 | } 136 | } 137 | }); 138 | } 139 | }); 140 | } 141 | 142 | return importSpecifierToFragments; 143 | }; 144 | 145 | function getFragmentsInSource( 146 | src: ts.SourceFile, 147 | typeChecker: ts.TypeChecker, 148 | info: ts.server.PluginCreateInfo 149 | ): Array { 150 | let fragments: Array = []; 151 | const callExpressions = findAllCallExpressions(src, info, false); 152 | 153 | const symbol = typeChecker.getSymbolAtLocation(src); 154 | if (!symbol) return []; 155 | 156 | const exports = typeChecker.getExportsOfModule(symbol); 157 | const exportedNames = exports.map(symb => symb.name); 158 | const nodes = callExpressions.nodes.filter(x => { 159 | let parent = x.node.parent; 160 | while ( 161 | parent && 162 | !ts.isSourceFile(parent) && 163 | !ts.isVariableDeclaration(parent) 164 | ) { 165 | parent = parent.parent; 166 | } 167 | 168 | if (ts.isVariableDeclaration(parent)) { 169 | return exportedNames.includes(parent.name.getText()); 170 | } else { 171 | return false; 172 | } 173 | }); 174 | 175 | nodes.forEach(node => { 176 | const text = resolveTemplate(node.node, src.fileName, info).combinedText; 177 | try { 178 | const parsed = parse(text, { noLocation: true }); 179 | if (parsed.definitions.every(x => x.kind === Kind.FRAGMENT_DEFINITION)) { 180 | fragments = fragments.concat(parsed.definitions as any); 181 | } 182 | } catch (e) { 183 | return; 184 | } 185 | }); 186 | 187 | return fragments; 188 | } 189 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/graphql/getFragmentSpreadSuggestions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, 3 | CompletionItemKind, 4 | ContextToken, 5 | ContextTokenUnion, 6 | Maybe, 7 | RuleKinds, 8 | getDefinitionState, 9 | } from 'graphql-language-service'; 10 | import { 11 | FragmentDefinitionNode, 12 | GraphQLArgument, 13 | GraphQLCompositeType, 14 | GraphQLDirective, 15 | GraphQLEnumValue, 16 | GraphQLField, 17 | GraphQLInputFieldMap, 18 | GraphQLInterfaceType, 19 | GraphQLObjectType, 20 | GraphQLSchema, 21 | GraphQLType, 22 | doTypesOverlap, 23 | isCompositeType, 24 | } from 'graphql'; 25 | 26 | /** 27 | * This part is vendored from https://github.com/graphql/graphiql/blob/main/packages/graphql-language-service/src/interface/autocompleteUtils.ts#L97 28 | */ 29 | type CompletionItemBase = { 30 | label: string; 31 | isDeprecated?: boolean; 32 | }; 33 | 34 | // Create the expected hint response given a possible list and a token 35 | function hintList( 36 | token: ContextTokenUnion, 37 | list: Array 38 | ): Array { 39 | return filterAndSortList(list, normalizeText(token.string)); 40 | } 41 | 42 | // Given a list of hint entries and currently typed text, sort and filter to 43 | // provide a concise list. 44 | function filterAndSortList( 45 | list: Array, 46 | text: string 47 | ): Array { 48 | if (!text) { 49 | return filterNonEmpty(list, entry => !entry.isDeprecated); 50 | } 51 | 52 | const byProximity = list.map(entry => ({ 53 | proximity: getProximity(normalizeText(entry.label), text), 54 | entry, 55 | })); 56 | 57 | return filterNonEmpty( 58 | filterNonEmpty(byProximity, pair => pair.proximity <= 2), 59 | pair => !pair.entry.isDeprecated 60 | ) 61 | .sort( 62 | (a, b) => 63 | (a.entry.isDeprecated ? 1 : 0) - (b.entry.isDeprecated ? 1 : 0) || 64 | a.proximity - b.proximity || 65 | a.entry.label.length - b.entry.label.length 66 | ) 67 | .map(pair => pair.entry); 68 | } 69 | 70 | // Filters the array by the predicate, unless it results in an empty array, 71 | // in which case return the original array. 72 | function filterNonEmpty( 73 | array: Array, 74 | predicate: (entry: T) => boolean 75 | ): Array { 76 | const filtered = array.filter(predicate); 77 | return filtered.length === 0 ? array : filtered; 78 | } 79 | 80 | function normalizeText(text: string): string { 81 | return text.toLowerCase().replace(/\W/g, ''); 82 | } 83 | 84 | // Determine a numeric proximity for a suggestion based on current text. 85 | function getProximity(suggestion: string, text: string): number { 86 | // start with lexical distance 87 | let proximity = lexicalDistance(text, suggestion); 88 | if (suggestion.length > text.length) { 89 | // do not penalize long suggestions. 90 | proximity -= suggestion.length - text.length - 1; 91 | // penalize suggestions not starting with this phrase 92 | proximity += suggestion.indexOf(text) === 0 ? 0 : 0.5; 93 | } 94 | return proximity; 95 | } 96 | 97 | /** 98 | * Computes the lexical distance between strings A and B. 99 | * 100 | * The "distance" between two strings is given by counting the minimum number 101 | * of edits needed to transform string A into string B. An edit can be an 102 | * insertion, deletion, or substitution of a single character, or a swap of two 103 | * adjacent characters. 104 | * 105 | * This distance can be useful for detecting typos in input or sorting 106 | * 107 | * @param {string} a 108 | * @param {string} b 109 | * @return {int} distance in number of edits 110 | */ 111 | function lexicalDistance(a: string, b: string): number { 112 | let i; 113 | let j; 114 | const d = []; 115 | const aLength = a.length; 116 | const bLength = b.length; 117 | 118 | for (i = 0; i <= aLength; i++) { 119 | d[i] = [i]; 120 | } 121 | 122 | for (j = 1; j <= bLength; j++) { 123 | d[0]![j] = j; 124 | } 125 | 126 | for (i = 1; i <= aLength; i++) { 127 | for (j = 1; j <= bLength; j++) { 128 | const cost = a[i - 1] === b[j - 1] ? 0 : 1; 129 | 130 | d[i]![j] = Math.min( 131 | d[i - 1]![j]! + 1, 132 | d[i]![j - 1]! + 1, 133 | d[i - 1]![j - 1]! + cost 134 | ); 135 | 136 | if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) { 137 | d[i]![j] = Math.min(d[i]![j]!, d[i - 2]![j - 2]! + cost); 138 | } 139 | } 140 | } 141 | 142 | return d[aLength]![bLength]!; 143 | } 144 | 145 | export type AllTypeInfo = { 146 | type: Maybe; 147 | parentType: Maybe; 148 | inputType: Maybe; 149 | directiveDef: Maybe; 150 | fieldDef: Maybe>; 151 | enumValue: Maybe; 152 | argDef: Maybe; 153 | argDefs: Maybe; 154 | objectFieldDefs: Maybe; 155 | interfaceDef: Maybe; 156 | objectTypeDef: Maybe; 157 | }; 158 | 159 | /** 160 | * This is vendored from https://github.com/graphql/graphiql/blob/main/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts#L779 161 | */ 162 | export function getSuggestionsForFragmentSpread( 163 | token: ContextToken, 164 | typeInfo: AllTypeInfo, 165 | schema: GraphQLSchema, 166 | queryText: string, 167 | fragments: FragmentDefinitionNode[] 168 | ): Array { 169 | if (!queryText) { 170 | return []; 171 | } 172 | 173 | const typeMap = schema.getTypeMap(); 174 | const defState = getDefinitionState(token.state); 175 | 176 | // Filter down to only the fragments which may exist here. 177 | const relevantFrags = fragments.filter( 178 | frag => 179 | // Only include fragments with known types. 180 | typeMap[frag.typeCondition.name.value] && 181 | // Only include fragments which are not cyclic. 182 | !( 183 | defState && 184 | defState.kind === RuleKinds.FRAGMENT_DEFINITION && 185 | defState.name === frag.name.value 186 | ) && 187 | // Only include fragments which could possibly be spread here. 188 | isCompositeType(typeInfo.parentType) && 189 | isCompositeType(typeMap[frag.typeCondition.name.value]) && 190 | doTypesOverlap( 191 | schema, 192 | typeInfo.parentType, 193 | typeMap[frag.typeCondition.name.value] as GraphQLCompositeType 194 | ) 195 | ); 196 | 197 | return hintList( 198 | token, 199 | relevantFrags.map(frag => ({ 200 | label: frag.name.value, 201 | detail: String(typeMap[frag.typeCondition.name.value]), 202 | documentation: `fragment ${frag.name.value} on ${frag.typeCondition.name.value}`, 203 | kind: CompletionItemKind.Field, 204 | type: typeMap[frag.typeCondition.name.value], 205 | })) 206 | ); 207 | } 208 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/graphql/getSchema.ts: -------------------------------------------------------------------------------- 1 | import type { Stats, PathLike } from 'node:fs'; 2 | import fs from 'node:fs/promises'; 3 | import path from 'path'; 4 | 5 | import type { IntrospectionQuery } from 'graphql'; 6 | 7 | import { 8 | type SchemaLoaderResult, 9 | type SchemaRef as _SchemaRef, 10 | type GraphQLSPConfig, 11 | loadRef, 12 | minifyIntrospection, 13 | outputIntrospectionFile, 14 | resolveTypeScriptRootDir, 15 | } from '@gql.tada/internal'; 16 | 17 | import { ts } from '../ts'; 18 | import { Logger } from '../index'; 19 | 20 | const statFile = ( 21 | file: PathLike, 22 | predicate: (stat: Stats) => boolean 23 | ): Promise => { 24 | return fs 25 | .stat(file) 26 | .then(predicate) 27 | .catch(() => false); 28 | }; 29 | 30 | const touchFile = async (file: PathLike): Promise => { 31 | try { 32 | const now = new Date(); 33 | await fs.utimes(file, now, now); 34 | } catch (_error) {} 35 | }; 36 | 37 | /** Writes a file to a swapfile then moves it into place to prevent excess change events. */ 38 | export const swapWrite = async ( 39 | target: PathLike, 40 | contents: string 41 | ): Promise => { 42 | if (!(await statFile(target, stat => stat.isFile()))) { 43 | // If the file doesn't exist, we can write directly, and not 44 | // try-catch so the error falls through 45 | await fs.writeFile(target, contents); 46 | } else { 47 | // If the file exists, we write to a swap-file, then rename (i.e. move) 48 | // the file into place. No try-catch around `writeFile` for proper 49 | // directory/permission errors 50 | const tempTarget = target + '.tmp'; 51 | await fs.writeFile(tempTarget, contents); 52 | try { 53 | await fs.rename(tempTarget, target); 54 | } catch (error) { 55 | await fs.unlink(tempTarget); 56 | throw error; 57 | } finally { 58 | // When we move the file into place, we also update its access and 59 | // modification time manually, in case the rename doesn't trigger 60 | // a change event 61 | await touchFile(target); 62 | } 63 | } 64 | }; 65 | 66 | async function saveTadaIntrospection( 67 | introspection: IntrospectionQuery, 68 | tadaOutputLocation: string, 69 | disablePreprocessing: boolean, 70 | logger: Logger 71 | ) { 72 | const minified = minifyIntrospection(introspection); 73 | const contents = outputIntrospectionFile(minified, { 74 | fileType: tadaOutputLocation, 75 | shouldPreprocess: !disablePreprocessing, 76 | }); 77 | 78 | let output = tadaOutputLocation; 79 | 80 | if (await statFile(output, stat => stat.isDirectory())) { 81 | output = path.join(output, 'introspection.d.ts'); 82 | } else if ( 83 | !(await statFile(path.dirname(output), stat => stat.isDirectory())) 84 | ) { 85 | logger(`Output file is not inside a directory @ ${output}`); 86 | return; 87 | } 88 | 89 | try { 90 | await swapWrite(output, contents); 91 | logger(`Introspection saved to path @ ${output}`); 92 | } catch (error) { 93 | logger(`Failed to write introspection @ ${error}`); 94 | } 95 | } 96 | 97 | export type SchemaRef = _SchemaRef; 98 | 99 | export const loadSchema = ( 100 | // TODO: abstract info away 101 | info: ts.server.PluginCreateInfo, 102 | origin: GraphQLSPConfig, 103 | logger: Logger 104 | ): _SchemaRef => { 105 | const ref = loadRef(origin); 106 | 107 | (async () => { 108 | const rootPath = 109 | (await resolveTypeScriptRootDir(info.project.getProjectName())) || 110 | path.dirname(info.project.getProjectName()); 111 | 112 | const tadaDisablePreprocessing = 113 | info.config.tadaDisablePreprocessing ?? false; 114 | const tadaOutputLocation = 115 | info.config.tadaOutputLocation && 116 | path.resolve(rootPath, info.config.tadaOutputLocation); 117 | 118 | logger('Got root-directory to resolve schema from: ' + rootPath); 119 | logger('Resolving schema from "schema" config: ' + JSON.stringify(origin)); 120 | 121 | try { 122 | logger(`Loading schema...`); 123 | await ref.load({ rootPath }); 124 | } catch (error) { 125 | logger(`Failed to load schema: ${error}`); 126 | } 127 | 128 | if (ref.current) { 129 | if (ref.current && ref.current.tadaOutputLocation !== undefined) { 130 | saveTadaIntrospection( 131 | ref.current.introspection, 132 | tadaOutputLocation, 133 | tadaDisablePreprocessing, 134 | logger 135 | ); 136 | } 137 | } else if (ref.multi) { 138 | Object.values(ref.multi).forEach(value => { 139 | if (!value) return; 140 | 141 | if (value.tadaOutputLocation) { 142 | saveTadaIntrospection( 143 | value.introspection, 144 | path.resolve(rootPath, value.tadaOutputLocation), 145 | tadaDisablePreprocessing, 146 | logger 147 | ); 148 | } 149 | }); 150 | } 151 | 152 | ref.autoupdate({ rootPath }, (schemaRef, value) => { 153 | if (!value) return; 154 | 155 | if (value.tadaOutputLocation) { 156 | const found = schemaRef.multi 157 | ? schemaRef.multi[value.name as string] 158 | : schemaRef.current; 159 | if (!found) return; 160 | saveTadaIntrospection( 161 | found.introspection, 162 | path.resolve(rootPath, value.tadaOutputLocation), 163 | tadaDisablePreprocessing, 164 | logger 165 | ); 166 | } 167 | }); 168 | })(); 169 | 170 | return ref as any; 171 | }; 172 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaOrigin } from '@gql.tada/internal'; 2 | 3 | import { ts, init as initTypeScript } from './ts'; 4 | import { loadSchema } from './graphql/getSchema'; 5 | import { getGraphQLCompletions } from './autoComplete'; 6 | import { getGraphQLQuickInfo } from './quickInfo'; 7 | import { ALL_DIAGNOSTICS, getGraphQLDiagnostics } from './diagnostics'; 8 | import { templates } from './ast/templates'; 9 | import { getPersistedCodeFixAtPosition } from './persisted'; 10 | 11 | function createBasicDecorator(info: ts.server.PluginCreateInfo) { 12 | const proxy: ts.LanguageService = Object.create(null); 13 | for (let k of Object.keys(info.languageService) as Array< 14 | keyof ts.LanguageService 15 | >) { 16 | const x = info.languageService[k]!; 17 | // @ts-expect-error - JS runtime trickery which is tricky to type tersely 18 | proxy[k] = (...args: Array<{}>) => x.apply(info.languageService, args); 19 | } 20 | 21 | return proxy; 22 | } 23 | 24 | export type Logger = (msg: string) => void; 25 | 26 | interface Config { 27 | schema: SchemaOrigin; 28 | schemas: SchemaOrigin[]; 29 | tadaDisablePreprocessing?: boolean; 30 | templateIsCallExpression?: boolean; 31 | shouldCheckForColocatedFragments?: boolean; 32 | template?: string; 33 | trackFieldUsage?: boolean; 34 | tadaOutputLocation?: string; 35 | } 36 | 37 | function create(info: ts.server.PluginCreateInfo) { 38 | const logger: Logger = (msg: string) => 39 | info.project.projectService.logger.info(`[GraphQLSP] ${msg}`); 40 | const config: Config = info.config; 41 | 42 | logger('config: ' + JSON.stringify(config)); 43 | if (!config.schema && !config.schemas) { 44 | logger('Missing "schema" option in configuration.'); 45 | throw new Error('Please provide a GraphQL Schema!'); 46 | } 47 | 48 | logger('Setting up the GraphQL Plugin'); 49 | 50 | if (config.template) { 51 | templates.add(config.template); 52 | } 53 | 54 | const proxy = createBasicDecorator(info); 55 | 56 | const schema = loadSchema(info, config, logger); 57 | 58 | proxy.getSemanticDiagnostics = (filename: string): ts.Diagnostic[] => { 59 | const originalDiagnostics = 60 | info.languageService.getSemanticDiagnostics(filename); 61 | 62 | const hasGraphQLDiagnostics = originalDiagnostics.some(x => 63 | ALL_DIAGNOSTICS.includes(x.code) 64 | ); 65 | if (hasGraphQLDiagnostics) return originalDiagnostics; 66 | 67 | const graphQLDiagnostics = getGraphQLDiagnostics(filename, schema, info); 68 | 69 | return graphQLDiagnostics 70 | ? [...graphQLDiagnostics, ...originalDiagnostics] 71 | : originalDiagnostics; 72 | }; 73 | 74 | proxy.getCompletionsAtPosition = ( 75 | filename: string, 76 | cursorPosition: number, 77 | options: any 78 | ): ts.WithMetadata | undefined => { 79 | const completions = getGraphQLCompletions( 80 | filename, 81 | cursorPosition, 82 | schema, 83 | info 84 | ); 85 | 86 | if (completions && completions.entries.length) { 87 | return completions; 88 | } else { 89 | return ( 90 | info.languageService.getCompletionsAtPosition( 91 | filename, 92 | cursorPosition, 93 | options 94 | ) || { 95 | isGlobalCompletion: false, 96 | isMemberCompletion: false, 97 | isNewIdentifierLocation: false, 98 | entries: [], 99 | } 100 | ); 101 | } 102 | }; 103 | 104 | proxy.getEditsForRefactor = ( 105 | filename, 106 | formatOptions, 107 | positionOrRange, 108 | refactorName, 109 | actionName, 110 | preferences, 111 | interactive 112 | ) => { 113 | const original = info.languageService.getEditsForRefactor( 114 | filename, 115 | formatOptions, 116 | positionOrRange, 117 | refactorName, 118 | actionName, 119 | preferences, 120 | interactive 121 | ); 122 | 123 | const codefix = getPersistedCodeFixAtPosition( 124 | filename, 125 | typeof positionOrRange === 'number' 126 | ? positionOrRange 127 | : positionOrRange.pos, 128 | info 129 | ); 130 | if (!codefix) return original; 131 | return { 132 | edits: [ 133 | { 134 | fileName: filename, 135 | textChanges: [{ newText: codefix.replacement, span: codefix.span }], 136 | }, 137 | ], 138 | }; 139 | }; 140 | 141 | proxy.getApplicableRefactors = ( 142 | filename, 143 | positionOrRange, 144 | preferences, 145 | reason, 146 | kind, 147 | includeInteractive 148 | ) => { 149 | const original = info.languageService.getApplicableRefactors( 150 | filename, 151 | positionOrRange, 152 | preferences, 153 | reason, 154 | kind, 155 | includeInteractive 156 | ); 157 | 158 | const codefix = getPersistedCodeFixAtPosition( 159 | filename, 160 | typeof positionOrRange === 'number' 161 | ? positionOrRange 162 | : positionOrRange.pos, 163 | info 164 | ); 165 | 166 | if (codefix) { 167 | return [ 168 | { 169 | name: 'GraphQL', 170 | description: 'Operations specific to gql.tada!', 171 | actions: [ 172 | { 173 | name: 'Insert document-id', 174 | description: 175 | 'Generate a document-id for your persisted-operation, by default a SHA256 hash.', 176 | }, 177 | ], 178 | inlineable: true, 179 | }, 180 | ...original, 181 | ]; 182 | } else { 183 | return original; 184 | } 185 | }; 186 | 187 | proxy.getQuickInfoAtPosition = (filename: string, cursorPosition: number) => { 188 | const quickInfo = getGraphQLQuickInfo( 189 | filename, 190 | cursorPosition, 191 | schema, 192 | info 193 | ); 194 | 195 | if (quickInfo) return quickInfo; 196 | 197 | return info.languageService.getQuickInfoAtPosition( 198 | filename, 199 | cursorPosition 200 | ); 201 | }; 202 | 203 | logger('proxy: ' + JSON.stringify(proxy)); 204 | 205 | return proxy; 206 | } 207 | 208 | const init: ts.server.PluginModuleFactory = ts => { 209 | initTypeScript(ts); 210 | return { create }; 211 | }; 212 | 213 | export default init; 214 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/quickInfo.ts: -------------------------------------------------------------------------------- 1 | import { ts } from './ts'; 2 | import { getHoverInformation } from 'graphql-language-service'; 3 | import { GraphQLSchema } from 'graphql'; 4 | 5 | import { 6 | bubbleUpCallExpression, 7 | bubbleUpTemplate, 8 | findNode, 9 | getSchemaName, 10 | getSource, 11 | } from './ast'; 12 | 13 | import * as checks from './ast/checks'; 14 | import { resolveTemplate } from './ast/resolve'; 15 | import { getToken } from './ast/token'; 16 | import { Cursor } from './ast/cursor'; 17 | import { templates } from './ast/templates'; 18 | import { SchemaRef } from './graphql/getSchema'; 19 | 20 | export function getGraphQLQuickInfo( 21 | filename: string, 22 | cursorPosition: number, 23 | schema: SchemaRef, 24 | info: ts.server.PluginCreateInfo 25 | ): ts.QuickInfo | undefined { 26 | const isCallExpression = info.config.templateIsCallExpression ?? true; 27 | const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 28 | 29 | const source = getSource(info, filename); 30 | if (!source) return undefined; 31 | 32 | let node = findNode(source, cursorPosition); 33 | if (!node) return undefined; 34 | 35 | node = isCallExpression 36 | ? bubbleUpCallExpression(node) 37 | : bubbleUpTemplate(node); 38 | 39 | let cursor, text, schemaToUse: GraphQLSchema | undefined; 40 | if (isCallExpression && checks.isGraphQLCall(node, typeChecker)) { 41 | const typeChecker = info.languageService.getProgram()?.getTypeChecker(); 42 | const schemaName = getSchemaName(node, typeChecker); 43 | 44 | schemaToUse = 45 | schemaName && schema.multi[schemaName] 46 | ? schema.multi[schemaName]?.schema 47 | : schema.current?.schema; 48 | 49 | const foundToken = getToken(node.arguments[0], cursorPosition); 50 | if (!schemaToUse || !foundToken) return undefined; 51 | 52 | text = node.arguments[0].getText(); 53 | cursor = new Cursor(foundToken.line, foundToken.start - 1); 54 | } else if (!isCallExpression && checks.isGraphQLTag(node)) { 55 | const foundToken = getToken(node.template, cursorPosition); 56 | if (!foundToken || !schema.current) return undefined; 57 | 58 | const { combinedText, resolvedSpans } = resolveTemplate( 59 | node, 60 | filename, 61 | info 62 | ); 63 | 64 | const amountOfLines = resolvedSpans 65 | .filter( 66 | x => 67 | x.original.start < cursorPosition && 68 | x.original.start + x.original.length < cursorPosition 69 | ) 70 | .reduce((acc, span) => acc + (span.lines - 1), 0); 71 | 72 | foundToken.line = foundToken.line + amountOfLines; 73 | text = combinedText; 74 | cursor = new Cursor(foundToken.line, foundToken.start - 1); 75 | schemaToUse = schema.current.schema; 76 | } else { 77 | return undefined; 78 | } 79 | 80 | const hoverInfo = getHoverInformation(schemaToUse, text, cursor); 81 | 82 | return { 83 | kind: ts.ScriptElementKind.label, 84 | textSpan: { 85 | start: cursorPosition, 86 | length: 1, 87 | }, 88 | kindModifiers: 'text', 89 | documentation: Array.isArray(hoverInfo) 90 | ? hoverInfo.map(item => ({ kind: 'text', text: item as string })) 91 | : [{ kind: 'text', text: hoverInfo as string }], 92 | } as ts.QuickInfo; 93 | } 94 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ts/index.d.ts: -------------------------------------------------------------------------------- 1 | import typescript from 'typescript/lib/tsserverlibrary'; 2 | export declare function init(modules: { typescript: typeof typescript }): void; 3 | export { typescript as ts }; 4 | -------------------------------------------------------------------------------- /packages/graphqlsp/src/ts/index.js: -------------------------------------------------------------------------------- 1 | export var ts; 2 | export function init(modules) { 3 | ts = modules.typescript; 4 | } 5 | -------------------------------------------------------------------------------- /packages/graphqlsp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "target": "es2019", 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowJs": true, 10 | "strict": true, 11 | "noUncheckedIndexedAccess": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'test/e2e/**' 4 | -------------------------------------------------------------------------------- /scripts/changelog.js: -------------------------------------------------------------------------------- 1 | const { config } = require('dotenv'); 2 | const { getInfo } = require('@changesets/get-github-info'); 3 | 4 | config(); 5 | 6 | const REPO = '0no-co/GraphQLSP'; 7 | const SEE_LINE = /^See:\s*(.*)/i; 8 | const TRAILING_CHAR = /[.;:]$/g; 9 | const listFormatter = new Intl.ListFormat('en-US'); 10 | 11 | const getSummaryLines = cs => { 12 | let lines = cs.summary.trim().split(/\r?\n/); 13 | if (!lines.some(line => /```/.test(line))) { 14 | lines = lines.map(l => l.trim()).filter(Boolean); 15 | const size = lines.length; 16 | if (size > 0) { 17 | lines[size - 1] = lines[size - 1].replace(TRAILING_CHAR, ''); 18 | } 19 | } 20 | return lines; 21 | }; 22 | 23 | /** Creates a "(See X)" string from a template */ 24 | const templateSeeRef = links => { 25 | const humanReadableLinks = links.filter(Boolean).map(link => { 26 | if (typeof link === 'string') return link; 27 | return link.pull || link.commit; 28 | }); 29 | 30 | const size = humanReadableLinks.length; 31 | if (size === 0) return ''; 32 | 33 | const str = listFormatter.format(humanReadableLinks); 34 | return `(See ${str})`; 35 | }; 36 | 37 | const changelogFunctions = { 38 | getDependencyReleaseLine: async (changesets, dependenciesUpdated) => { 39 | if (dependenciesUpdated.length === 0) return ''; 40 | 41 | const dependenciesLinks = await Promise.all( 42 | changesets.map(async cs => { 43 | if (!cs.commit) return undefined; 44 | 45 | const lines = getSummaryLines(cs); 46 | const prLine = lines.find(line => SEE_LINE.test(line)); 47 | if (prLine) { 48 | const match = prLine.match(SEE_LINE); 49 | return (match && match[1].trim()) || undefined; 50 | } 51 | 52 | const { links } = await getInfo({ 53 | repo: REPO, 54 | commit: cs.commit, 55 | }); 56 | 57 | return links; 58 | }) 59 | ); 60 | 61 | let changesetLink = '- Updated dependencies'; 62 | 63 | const seeRef = templateSeeRef(dependenciesLinks); 64 | if (seeRef) changesetLink += ` ${seeRef}`; 65 | 66 | const detailsLinks = dependenciesUpdated.map(dep => { 67 | return ` - ${dep.name}@${dep.newVersion}`; 68 | }); 69 | 70 | return [changesetLink, ...detailsLinks].join('\n'); 71 | }, 72 | getReleaseLine: async (changeset, type) => { 73 | let pull, commit, user; 74 | 75 | const lines = getSummaryLines(changeset); 76 | const prLineIndex = lines.findIndex(line => SEE_LINE.test(line)); 77 | if (prLineIndex > -1) { 78 | const match = lines[prLineIndex].match(SEE_LINE); 79 | pull = (match && match[1].trim()) || undefined; 80 | lines.splice(prLineIndex, 1); 81 | } 82 | 83 | const [firstLine, ...futureLines] = lines; 84 | 85 | if (changeset.commit && !pull) { 86 | const { links } = await getInfo({ 87 | repo: REPO, 88 | commit: changeset.commit, 89 | }); 90 | 91 | pull = links.pull || undefined; 92 | commit = links.commit || undefined; 93 | user = links.user || undefined; 94 | } 95 | 96 | let annotation = ''; 97 | if (type === 'patch' && /^\s*fix/i.test(firstLine)) { 98 | annotation = '⚠️ '; 99 | } 100 | 101 | let str = `- ${annotation}${firstLine}`; 102 | if (futureLines.length > 0) { 103 | str += `\n${futureLines.map(l => ` ${l}`).join('\n')}`; 104 | } 105 | 106 | const endsWithParagraph = /(?<=(?:[!;?.]|```) *)$/g; 107 | if (user && !endsWithParagraph) { 108 | str += `, by ${user}`; 109 | } else { 110 | str += `\nSubmitted by ${user}`; 111 | } 112 | 113 | if (pull || commit) { 114 | const seeRef = templateSeeRef([pull || commit]); 115 | if (seeRef) str += ` ${seeRef}`; 116 | } 117 | 118 | return str; 119 | }, 120 | }; 121 | 122 | module.exports = { 123 | ...changelogFunctions, 124 | default: changelogFunctions, 125 | }; 126 | -------------------------------------------------------------------------------- /scripts/launch-debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Check if code is in PATH 4 | 5 | if ! command -v code &> /dev/null 6 | then 7 | echo "Make sure to add VS Code to your PATH:" 8 | echo "https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line" 9 | exit 10 | fi 11 | 12 | TSS_DEBUG=9559 code --user-data-dir ~/.vscode-debug/ packages/example 13 | -------------------------------------------------------------------------------- /scripts/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path/posix'; 3 | import { readFileSync } from 'node:fs'; 4 | 5 | import * as prettier from 'prettier'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import resolve from '@rollup/plugin-node-resolve'; 8 | import babel from '@rollup/plugin-babel'; 9 | import terser from '@rollup/plugin-terser'; 10 | import cjsCheck from 'rollup-plugin-cjs-check'; 11 | import dts from 'rollup-plugin-dts'; 12 | 13 | const normalize = name => [] 14 | .concat(name) 15 | .join(' ') 16 | .replace(/[@\s/.]+/g, ' ') 17 | .trim() 18 | .replace(/\s+/, '-') 19 | .toLowerCase(); 20 | 21 | const extension = name => { 22 | if (/\.d.ts$/.test(name)) { 23 | return '.d.ts'; 24 | } else if (/\.module.js$/.test(name)) { 25 | return '.module.js'; 26 | } else { 27 | return path.extname(name); 28 | } 29 | }; 30 | 31 | const meta = JSON.parse(readFileSync('package.json')); 32 | const name = normalize(meta.name); 33 | 34 | const externalModules = [ 35 | 'typescript', 36 | ...Object.keys(meta.dependencies || {}), 37 | ...Object.keys(meta.peerDependencies || {}), 38 | ]; 39 | 40 | const external = new RegExp(`^(${externalModules.join('|')})($|/)`); 41 | 42 | const exports = {}; 43 | for (const key in meta.exports) { 44 | const entry = meta.exports[key]; 45 | if (typeof entry === 'object' && !!entry.source) { 46 | const entryPath = normalize(key); 47 | const entryName = normalize([name, entryPath]); 48 | exports[entryName] = { 49 | path: entryPath, 50 | ...entry, 51 | }; 52 | } 53 | } 54 | 55 | const commonConfig = { 56 | input: Object.entries(exports).reduce((input, [exportName, entry]) => { 57 | input[exportName] = entry.source; 58 | return input; 59 | }, {}), 60 | onwarn: () => {}, 61 | external(id) { 62 | return external.test(id); 63 | }, 64 | treeshake: { 65 | unknownGlobalSideEffects: false, 66 | tryCatchDeoptimization: false, 67 | moduleSideEffects: false, 68 | }, 69 | }; 70 | 71 | const commonPlugins = [ 72 | resolve({ 73 | extensions: ['.mjs', '.js', '.ts'], 74 | mainFields: ['module', 'jsnext', 'main'], 75 | preferBuiltins: false, 76 | browser: true, 77 | }), 78 | 79 | commonjs({ 80 | ignoreGlobal: true, 81 | include: /\/node_modules\//, 82 | }), 83 | ]; 84 | 85 | const commonOutput = { 86 | dir: './', 87 | exports: 'auto', 88 | sourcemap: true, 89 | sourcemapExcludeSources: false, 90 | hoistTransitiveImports: false, 91 | indent: false, 92 | freeze: false, 93 | strict: false, 94 | generatedCode: { 95 | preset: 'es5', 96 | reservedNamesAsProps: false, 97 | objectShorthand: false, 98 | constBindings: false, 99 | }, 100 | }; 101 | 102 | const outputPlugins = [ 103 | { 104 | name: 'outputPackageJsons', 105 | async writeBundle() { 106 | for (const key in exports) { 107 | const entry = exports[key]; 108 | if (entry.path) { 109 | const output = path.relative(entry.path, process.cwd()); 110 | const json = JSON.stringify({ 111 | name: key, 112 | private: true, 113 | version: '0.0.0', 114 | main: path.join(output, entry.require), 115 | module: path.join(output, entry.import), 116 | types: path.join(output, entry.types), 117 | source: path.join(output, entry.source), 118 | exports: { 119 | '.': { 120 | types: path.join(output, entry.types), 121 | import: path.join(output, entry.import), 122 | require: path.join(output, entry.require), 123 | source: path.join(output, entry.source), 124 | }, 125 | }, 126 | }, null, 2); 127 | 128 | await fs.mkdir(entry.path, { recursive: true }); 129 | await fs.writeFile(path.join(entry.path, 'package.json'), json); 130 | } 131 | } 132 | }, 133 | }, 134 | 135 | cjsCheck(), 136 | 137 | terser({ 138 | warnings: true, 139 | ecma: 2015, 140 | keep_fnames: true, 141 | ie8: false, 142 | compress: { 143 | pure_getters: true, 144 | toplevel: true, 145 | booleans_as_integers: false, 146 | keep_fnames: true, 147 | keep_fargs: true, 148 | if_return: false, 149 | ie8: false, 150 | sequences: false, 151 | loops: false, 152 | conditionals: false, 153 | join_vars: false, 154 | }, 155 | mangle: { 156 | module: true, 157 | keep_fnames: true, 158 | }, 159 | output: { 160 | beautify: true, 161 | braces: true, 162 | indent_level: 2, 163 | }, 164 | }), 165 | ]; 166 | 167 | export default [ 168 | { 169 | ...commonConfig, 170 | plugins: [ 171 | ...commonPlugins, 172 | babel({ 173 | babelrc: false, 174 | babelHelpers: 'bundled', 175 | extensions: ['mjs', 'js', 'jsx', 'ts', 'tsx'], 176 | exclude: 'node_modules/**', 177 | presets: [], 178 | plugins: [ 179 | '@babel/plugin-transform-typescript', 180 | '@babel/plugin-transform-block-scoping', 181 | ], 182 | }), 183 | ], 184 | output: [ 185 | { 186 | ...commonOutput, 187 | format: 'esm', 188 | chunkFileNames(chunk) { 189 | return `dist/chunks/[name]-chunk${extension(chunk.name) || '.mjs'}`; 190 | }, 191 | entryFileNames(chunk) { 192 | return chunk.isEntry 193 | ? path.normalize(exports[chunk.name].import) 194 | : `dist/[name].mjs`; 195 | }, 196 | plugins: outputPlugins, 197 | }, 198 | { 199 | ...commonOutput, 200 | format: 'cjs', 201 | esModule: true, 202 | externalLiveBindings: true, 203 | chunkFileNames(chunk) { 204 | return `dist/chunks/[name]-chunk${extension(chunk.name) || '.js'}`; 205 | }, 206 | entryFileNames(chunk) { 207 | return chunk.isEntry 208 | ? path.normalize(exports[chunk.name].require) 209 | : `dist/[name].js`; 210 | }, 211 | plugins: outputPlugins, 212 | }, 213 | ], 214 | }, 215 | 216 | { 217 | ...commonConfig, 218 | plugins: [ 219 | ...commonPlugins, 220 | dts(), 221 | ], 222 | output: { 223 | ...commonOutput, 224 | sourcemap: false, 225 | format: 'dts', 226 | chunkFileNames(chunk) { 227 | return `dist/chunks/[name]-chunk${extension(chunk.name) || '.d.ts'}`; 228 | }, 229 | entryFileNames(chunk) { 230 | return chunk.isEntry 231 | ? path.normalize(exports[chunk.name].types) 232 | : `dist/[name].d.ts`; 233 | }, 234 | plugins: [ 235 | { 236 | renderChunk(code, chunk) { 237 | if (chunk.fileName.endsWith('d.ts')) { 238 | const gqlImportRe = /(import\s+(?:[*\s{}\w\d]+)\s*from\s*'graphql';?)/g; 239 | code = code.replace(gqlImportRe, x => '/*!@ts-ignore*/\n' + x); 240 | 241 | code = prettier.format(code, { 242 | filepath: chunk.fileName, 243 | parser: 'typescript', 244 | singleQuote: true, 245 | tabWidth: 2, 246 | printWidth: 100, 247 | trailingComma: 'es5', 248 | }); 249 | 250 | return code; 251 | } 252 | }, 253 | }, 254 | ], 255 | }, 256 | }, 257 | ]; 258 | -------------------------------------------------------------------------------- /test/e2e/combinations.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, afterAll, beforeAll, it, describe } from 'vitest'; 2 | import { TSServer } from './server'; 3 | import path from 'node:path'; 4 | import fs from 'node:fs'; 5 | import url from 'node:url'; 6 | import ts from 'typescript/lib/tsserverlibrary'; 7 | 8 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 9 | 10 | const projectPath = path.resolve(__dirname, 'fixture-project'); 11 | describe('Fragment + operations', () => { 12 | const outfileCombinations = path.join(projectPath, 'Combination.ts'); 13 | 14 | let server: TSServer; 15 | beforeAll(async () => { 16 | server = new TSServer(projectPath, { debugLog: false }); 17 | 18 | server.sendCommand('open', { 19 | file: outfileCombinations, 20 | fileContent: '// empty', 21 | scriptKindName: 'TS', 22 | } satisfies ts.server.protocol.OpenRequestArgs); 23 | 24 | server.sendCommand('updateOpen', { 25 | openFiles: [ 26 | { 27 | file: outfileCombinations, 28 | fileContent: fs.readFileSync( 29 | path.join(projectPath, 'fixtures/Combination.ts'), 30 | 'utf-8' 31 | ), 32 | }, 33 | ], 34 | } satisfies ts.server.protocol.UpdateOpenRequestArgs); 35 | 36 | server.sendCommand('saveto', { 37 | file: outfileCombinations, 38 | tmpfile: outfileCombinations, 39 | } satisfies ts.server.protocol.SavetoRequestArgs); 40 | }); 41 | 42 | afterAll(() => { 43 | try { 44 | fs.unlinkSync(outfileCombinations); 45 | } catch {} 46 | }); 47 | 48 | it('gives semantic-diagnostics with preceding fragments', async () => { 49 | await server.waitForResponse( 50 | e => e.type === 'event' && e.event === 'semanticDiag' 51 | ); 52 | const res = server.responses 53 | .reverse() 54 | .find(resp => resp.type === 'event' && resp.event === 'semanticDiag'); 55 | expect(res?.body.diagnostics).toMatchInlineSnapshot(` 56 | [ 57 | { 58 | "category": "error", 59 | "code": 52001, 60 | "end": { 61 | "line": 7, 62 | "offset": 1, 63 | }, 64 | "start": { 65 | "line": 6, 66 | "offset": 5, 67 | }, 68 | "text": "Cannot query field \\"someUnknownField\\" on type \\"Post\\".", 69 | }, 70 | { 71 | "category": "error", 72 | "code": 52001, 73 | "end": { 74 | "line": 11, 75 | "offset": 10, 76 | }, 77 | "start": { 78 | "line": 11, 79 | "offset": 3, 80 | }, 81 | "text": "Cannot query field \\"someUnknownField\\" on type \\"Post\\".", 82 | }, 83 | { 84 | "category": "error", 85 | "code": 52001, 86 | "end": { 87 | "line": 17, 88 | "offset": 1, 89 | }, 90 | "start": { 91 | "line": 16, 92 | "offset": 7, 93 | }, 94 | "text": "Cannot query field \\"__typenam\\" on type \\"Post\\".", 95 | }, 96 | ] 97 | `); 98 | }, 30000); 99 | 100 | it('gives quick-info with preceding fragments', async () => { 101 | server.send({ 102 | seq: 9, 103 | type: 'request', 104 | command: 'quickinfo', 105 | arguments: { 106 | file: outfileCombinations, 107 | line: 14, 108 | offset: 7, 109 | }, 110 | }); 111 | 112 | await server.waitForResponse( 113 | response => 114 | response.type === 'response' && response.command === 'quickinfo' 115 | ); 116 | 117 | const res = server.responses 118 | .reverse() 119 | .find(resp => resp.type === 'response' && resp.command === 'quickinfo'); 120 | 121 | expect(res).toBeDefined(); 122 | expect(typeof res?.body).toEqual('object'); 123 | expect(res?.body.documentation).toEqual( 124 | `Query.posts: [Post]\n\nList out all posts` 125 | ); 126 | }, 30000); 127 | 128 | it('gives suggestions with preceding fragments', async () => { 129 | server.send({ 130 | seq: 10, 131 | type: 'request', 132 | command: 'completionInfo', 133 | arguments: { 134 | file: outfileCombinations, 135 | line: 15, 136 | offset: 7, 137 | includeExternalModuleExports: true, 138 | includeInsertTextCompletions: true, 139 | triggerKind: 1, 140 | }, 141 | }); 142 | 143 | await server.waitForResponse( 144 | response => 145 | response.type === 'response' && response.command === 'completionInfo' 146 | ); 147 | 148 | const res = server.responses 149 | .reverse() 150 | .find( 151 | resp => resp.type === 'response' && resp.command === 'completionInfo' 152 | ); 153 | 154 | expect(res).toBeDefined(); 155 | expect(typeof res?.body.entries).toEqual('object'); 156 | expect(res?.body.entries).toMatchInlineSnapshot(` 157 | [ 158 | { 159 | "kind": "var", 160 | "kindModifiers": "declare", 161 | "labelDetails": { 162 | "detail": " ID!", 163 | }, 164 | "name": "id", 165 | "sortText": "0id", 166 | }, 167 | { 168 | "kind": "var", 169 | "kindModifiers": "declare", 170 | "labelDetails": { 171 | "detail": " String!", 172 | }, 173 | "name": "title", 174 | "sortText": "1title", 175 | }, 176 | { 177 | "kind": "var", 178 | "kindModifiers": "declare", 179 | "labelDetails": { 180 | "detail": " String!", 181 | }, 182 | "name": "content", 183 | "sortText": "2content", 184 | }, 185 | { 186 | "kind": "var", 187 | "kindModifiers": "declare", 188 | "labelDetails": { 189 | "description": "The name of the current Object type at runtime.", 190 | "detail": " String!", 191 | }, 192 | "name": "__typename", 193 | "sortText": "3__typename", 194 | }, 195 | ] 196 | `); 197 | }, 30000); 198 | }); 199 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/__generated__/baseGraphQLSP.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { 4 | [K in keyof T]: T[K]; 5 | }; 6 | export type MakeOptional = Omit & { 7 | [SubKey in K]?: Maybe; 8 | }; 9 | export type MakeMaybe = Omit & { 10 | [SubKey in K]: Maybe; 11 | }; 12 | export type MakeEmpty< 13 | T extends { [key: string]: unknown }, 14 | K extends keyof T 15 | > = { [_ in K]?: never }; 16 | export type Incremental = 17 | | T 18 | | { 19 | [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; 20 | }; 21 | /** All built-in and custom scalars, mapped to their actual values */ 22 | export type Scalars = { 23 | ID: { input: string; output: string }; 24 | String: { input: string; output: string }; 25 | Boolean: { input: boolean; output: boolean }; 26 | Int: { input: number; output: number }; 27 | Float: { input: number; output: number }; 28 | }; 29 | 30 | /** Move a Pokémon can perform with the associated damage and type. */ 31 | export type Attack = { 32 | __typename: 'Attack'; 33 | damage?: Maybe; 34 | name?: Maybe; 35 | type?: Maybe; 36 | }; 37 | 38 | export type AttacksConnection = { 39 | __typename: 'AttacksConnection'; 40 | fast?: Maybe>>; 41 | special?: Maybe>>; 42 | }; 43 | 44 | /** Requirement that prevents an evolution through regular means of levelling up. */ 45 | export type EvolutionRequirement = { 46 | __typename: 'EvolutionRequirement'; 47 | amount?: Maybe; 48 | name?: Maybe; 49 | }; 50 | 51 | export type Pokemon = { 52 | __typename: 'Pokemon'; 53 | attacks?: Maybe; 54 | /** @deprecated And this is the reason why */ 55 | classification?: Maybe; 56 | evolutionRequirements?: Maybe>>; 57 | evolutions?: Maybe>>; 58 | /** Likelihood of an attempt to catch a Pokémon to fail. */ 59 | fleeRate?: Maybe; 60 | height?: Maybe; 61 | id: Scalars['ID']['output']; 62 | /** Maximum combat power a Pokémon may achieve at max level. */ 63 | maxCP?: Maybe; 64 | /** Maximum health points a Pokémon may achieve at max level. */ 65 | maxHP?: Maybe; 66 | name: Scalars['String']['output']; 67 | resistant?: Maybe>>; 68 | types?: Maybe>>; 69 | weaknesses?: Maybe>>; 70 | weight?: Maybe; 71 | }; 72 | 73 | export type PokemonDimension = { 74 | __typename: 'PokemonDimension'; 75 | maximum?: Maybe; 76 | minimum?: Maybe; 77 | }; 78 | 79 | /** Elemental property associated with either a Pokémon or one of their moves. */ 80 | export type PokemonType = 81 | | 'Bug' 82 | | 'Dark' 83 | | 'Dragon' 84 | | 'Electric' 85 | | 'Fairy' 86 | | 'Fighting' 87 | | 'Fire' 88 | | 'Flying' 89 | | 'Ghost' 90 | | 'Grass' 91 | | 'Ground' 92 | | 'Ice' 93 | | 'Normal' 94 | | 'Poison' 95 | | 'Psychic' 96 | | 'Rock' 97 | | 'Steel' 98 | | 'Water'; 99 | 100 | export type Query = { 101 | __typename: 'Query'; 102 | /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 103 | pokemon?: Maybe; 104 | /** List out all Pokémon, optionally in pages */ 105 | pokemons?: Maybe>>; 106 | }; 107 | 108 | export type QueryPokemonArgs = { 109 | id: Scalars['ID']['input']; 110 | }; 111 | 112 | export type QueryPokemonsArgs = { 113 | limit?: InputMaybe; 114 | skip?: InputMaybe; 115 | }; 116 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/fixtures/fragment.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './gql/gql'; 2 | 3 | // prettier-ignore 4 | export const PokemonFields = graphql(` 5 | fragment pokemonFields on Pokemon { 6 | id 7 | name 8 | fleeRate 9 | 10 | } 11 | `); 12 | 13 | export const Pokemon = () => {}; 14 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/fixtures/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResultOf, 3 | DocumentTypeDecoration, 4 | TypedDocumentNode, 5 | } from '@graphql-typed-document-node/core'; 6 | import { FragmentDefinitionNode } from 'graphql'; 7 | import { Incremental } from './graphql'; 8 | 9 | export type FragmentType< 10 | TDocumentType extends DocumentTypeDecoration 11 | > = TDocumentType extends DocumentTypeDecoration 12 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 13 | ? TKey extends string 14 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 15 | : never 16 | : never 17 | : never; 18 | 19 | // return non-nullable if `fragmentType` is non-nullable 20 | export function useFragment( 21 | _documentNode: DocumentTypeDecoration, 22 | fragmentType: FragmentType> 23 | ): TType; 24 | // return nullable if `fragmentType` is nullable 25 | export function useFragment( 26 | _documentNode: DocumentTypeDecoration, 27 | fragmentType: 28 | | FragmentType> 29 | | null 30 | | undefined 31 | ): TType | null | undefined; 32 | // return array of non-nullable if `fragmentType` is array of non-nullable 33 | export function useFragment( 34 | _documentNode: DocumentTypeDecoration, 35 | fragmentType: ReadonlyArray>> 36 | ): ReadonlyArray; 37 | // return array of nullable if `fragmentType` is array of nullable 38 | export function useFragment( 39 | _documentNode: DocumentTypeDecoration, 40 | fragmentType: 41 | | ReadonlyArray>> 42 | | null 43 | | undefined 44 | ): ReadonlyArray | null | undefined; 45 | export function useFragment( 46 | _documentNode: DocumentTypeDecoration, 47 | fragmentType: 48 | | FragmentType> 49 | | ReadonlyArray>> 50 | | null 51 | | undefined 52 | ): TType | ReadonlyArray | null | undefined { 53 | return fragmentType as any; 54 | } 55 | 56 | export function makeFragmentData< 57 | F extends DocumentTypeDecoration, 58 | FT extends ResultOf 59 | >(data: FT, _fragment: F): FragmentType { 60 | return data as FragmentType; 61 | } 62 | export function isFragmentReady( 63 | queryNode: DocumentTypeDecoration, 64 | fragmentNode: TypedDocumentNode, 65 | data: 66 | | FragmentType, any>> 67 | | null 68 | | undefined 69 | ): data is FragmentType { 70 | const deferredFields = ( 71 | queryNode as { 72 | __meta__?: { deferredFields: Record }; 73 | } 74 | ).__meta__?.deferredFields; 75 | 76 | if (!deferredFields) return true; 77 | 78 | const fragDef = fragmentNode.definitions[0] as 79 | | FragmentDefinitionNode 80 | | undefined; 81 | const fragName = fragDef?.name?.value; 82 | 83 | const fields = (fragName && deferredFields[fragName]) || []; 84 | return fields.length > 0 && fields.every(field => data && field in data); 85 | } 86 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/fixtures/gql/gql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as types from './graphql'; 3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | 5 | /** 6 | * Map of all GraphQL operations in the project. 7 | * 8 | * This map has several performance disadvantages: 9 | * 1. It is not tree-shakeable, so it will include all operations in the project. 10 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 | * 3. It does not support dead code elimination, so it will add unused operations. 12 | * 13 | * Therefore it is highly recommended to use the babel or swc plugin for production. 14 | */ 15 | const documents = { 16 | '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 | types.PokemonFieldsFragmentDoc, 18 | '\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n': 19 | types.PokDocument, 20 | }; 21 | 22 | /** 23 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 24 | * 25 | * 26 | * @example 27 | * ```ts 28 | * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 29 | * ``` 30 | * 31 | * The query argument is unknown! 32 | * Please regenerate the types. 33 | */ 34 | export function graphql(source: string): unknown; 35 | 36 | /** 37 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 38 | */ 39 | export function graphql( 40 | source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n' 41 | ): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n']; 42 | /** 43 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 44 | */ 45 | export function graphql( 46 | source: '\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n' 47 | ): (typeof documents)['\n query Pok($limit: Int!) {\n pokemons(limit: $limit) {\n id\n name\n fleeRate\n classification\n ...pokemonFields\n ...weaknessFields\n __typename\n }\n }\n']; 48 | 49 | export function graphql(source: string) { 50 | return (documents as any)[source] ?? {}; 51 | } 52 | 53 | export type DocumentType> = 54 | TDocumentNode extends DocumentNode ? TType : never; 55 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/fixtures/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-masking'; 2 | export * from './gql'; 3 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/fixtures/simple.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './gql/gql'; 2 | 3 | const x = graphql(` 4 | query Pok($limit: Int!) { 5 | pokemons(limit: $limit) { 6 | id 7 | name 8 | fleeRate 9 | classification 10 | ...pokemonFields 11 | __typename 12 | } 13 | } 14 | `); 15 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/fixtures/unused-fragment.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './gql/gql'; 2 | import { Pokemon } from './fragment'; 3 | 4 | const x = graphql(` 5 | query Pok($limit: Int!) { 6 | pokemons(limit: $limit) { 7 | id 8 | name 9 | } 10 | } 11 | `); 12 | 13 | console.log(Pokemon); 14 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "private": true, 4 | "dependencies": { 5 | "graphql": "^16.0.0", 6 | "@graphql-typed-document-node/core": "^3.0.0", 7 | "@0no-co/graphqlsp": "workspace:*", 8 | "@urql/core": "^4.0.4" 9 | }, 10 | "devDependencies": { 11 | "typescript": "^5.3.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /test/e2e/fixture-project-client-preset/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schema": "./schema.graphql", 7 | "disableTypegen": true, 8 | "trackFieldUsage": false 9 | } 10 | ], 11 | "target": "es2016", 12 | "esModuleInterop": true, 13 | "moduleResolution": "node", 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "skipLibCheck": true 17 | }, 18 | "exclude": ["node_modules", "fixtures"] 19 | } 20 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/fixtures/pokemon.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | import type { introspection } from '../pokemons'; 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: introspection; 6 | }>(); 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 | export { readFragment } from 'gql.tada'; 10 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/fixtures/simple-pokemon.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './pokemon'; 2 | 3 | // prettier-ignore 4 | const x = graphql(` 5 | query Pokemons($limit: Int!) { 6 | pokemons(limit: $limit) { 7 | id 8 | name 9 | 10 | fleeRate 11 | classification 12 | __typename 13 | } 14 | } 15 | `); 16 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/fixtures/simple-todo.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './todo'; 2 | 3 | // prettier-ignore 4 | const x = graphql(` 5 | query Todo($id: ID!) { 6 | todo(id: $id) { 7 | id 8 | 9 | } 10 | } 11 | `); 12 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/fixtures/star-import.ts: -------------------------------------------------------------------------------- 1 | import * as pokemon from './pokemon'; 2 | 3 | // prettier-ignore 4 | const x = pokemon.graphql(` 5 | query Pokemons($limit: Int!) { 6 | pokemons(limit: $limit) { 7 | id 8 | name 9 | fleeRate 10 | classification 11 | __typename 12 | } 13 | } 14 | `); 15 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/fixtures/todo.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | import type { introspection } from '../todos'; 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: introspection; 6 | }>(); 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 | export { readFragment } from 'gql.tada'; 10 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "private": true, 4 | "dependencies": { 5 | "graphql": "^16.0.0", 6 | "gql.tada": "1.6.0", 7 | "@graphql-typed-document-node/core": "^3.0.0", 8 | "@0no-co/graphqlsp": "workspace:*", 9 | "@urql/core": "^4.0.4" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.3.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/pokemon.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | import type { introspection } from './pokemons'; 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: introspection; 6 | }>(); 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 | export { readFragment } from 'gql.tada'; 10 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/pokemons.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | 4 | /** An IntrospectionQuery representation of your schema. 5 | * 6 | * @remarks 7 | * This is an introspection of your schema saved as a file by GraphQLSP. 8 | * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents. 9 | * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to 10 | * instead save to a .ts instead of a .d.ts file. 11 | */ 12 | export type introspection = { 13 | name: 'pokemons'; 14 | query: 'Query'; 15 | mutation: never; 16 | subscription: never; 17 | types: { 18 | 'Attack': { kind: 'OBJECT'; name: 'Attack'; fields: { 'damage': { name: 'damage'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'type': { name: 'type'; type: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; } }; }; }; 19 | 'AttacksConnection': { kind: 'OBJECT'; name: 'AttacksConnection'; fields: { 'fast': { name: 'fast'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; 'special': { name: 'special'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; }; }; 20 | 'Boolean': unknown; 21 | 'EvolutionRequirement': { kind: 'OBJECT'; name: 'EvolutionRequirement'; fields: { 'amount': { name: 'amount'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 22 | 'Float': unknown; 23 | 'ID': unknown; 24 | 'Int': unknown; 25 | 'Pokemon': { kind: 'OBJECT'; name: 'Pokemon'; fields: { 'attacks': { name: 'attacks'; type: { kind: 'OBJECT'; name: 'AttacksConnection'; ofType: null; } }; 'classification': { name: 'classification'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'evolutionRequirements': { name: 'evolutionRequirements'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'EvolutionRequirement'; ofType: null; }; } }; 'evolutions': { name: 'evolutions'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; 'fleeRate': { name: 'fleeRate'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'height': { name: 'height'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'maxCP': { name: 'maxCP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'maxHP': { name: 'maxHP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'resistant': { name: 'resistant'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'types': { name: 'types'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weaknesses': { name: 'weaknesses'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weight': { name: 'weight'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; }; }; 26 | 'PokemonDimension': { kind: 'OBJECT'; name: 'PokemonDimension'; fields: { 'maximum': { name: 'maximum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'minimum': { name: 'minimum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 27 | 'PokemonType': { name: 'PokemonType'; enumValues: 'Bug' | 'Dark' | 'Dragon' | 'Electric' | 'Fairy' | 'Fighting' | 'Fire' | 'Flying' | 'Ghost' | 'Grass' | 'Ground' | 'Ice' | 'Normal' | 'Poison' | 'Psychic' | 'Rock' | 'Steel' | 'Water'; }; 28 | 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'pokemon': { name: 'pokemon'; type: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; } }; 'pokemons': { name: 'pokemons'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; }; }; 29 | 'String': unknown; 30 | }; 31 | }; 32 | 33 | import * as gqlTada from 'gql.tada'; 34 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/pokemons.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/todo.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | // @ts-ignore 3 | import type { introspection } from './todos'; 4 | 5 | export const graphql = initGraphQLTada<{ 6 | introspection: introspection; 7 | }>(); 8 | 9 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 10 | export { readFragment } from 'gql.tada'; 11 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/todos.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | 4 | /** An IntrospectionQuery representation of your schema. 5 | * 6 | * @remarks 7 | * This is an introspection of your schema saved as a file by GraphQLSP. 8 | * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents. 9 | * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to 10 | * instead save to a .ts instead of a .d.ts file. 11 | */ 12 | export type introspection = { 13 | name: 'todos'; 14 | query: 'Query'; 15 | mutation: never; 16 | subscription: never; 17 | types: { 18 | 'Boolean': unknown; 19 | 'ID': unknown; 20 | 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'todo': { name: 'todo'; type: { kind: 'OBJECT'; name: 'Todo'; ofType: null; } }; }; }; 21 | 'String': unknown; 22 | 'Todo': { kind: 'OBJECT'; name: 'Todo'; fields: { 'completed': { name: 'completed'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'text': { name: 'text'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; 23 | }; 24 | }; 25 | 26 | import * as gqlTada from 'gql.tada'; 27 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/todos.graphql: -------------------------------------------------------------------------------- 1 | type Todo { 2 | id: ID! 3 | text: String! 4 | completed: Boolean! 5 | } 6 | 7 | type Query { 8 | """ 9 | Get a single Todo by its ID 10 | """ 11 | todo(id: ID!): Todo 12 | } -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada-multi-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schemas": [ 7 | { 8 | "name": "pokemons", 9 | "schema": "./pokemons.graphql", 10 | "tadaOutputLocation": "./pokemons.d.ts" 11 | }, 12 | { 13 | "name": "todos", 14 | "schema": "./todos.graphql", 15 | "tadaOutputLocation": "./todos.d.ts" 16 | } 17 | ] 18 | } 19 | ], 20 | "target": "es2016", 21 | "esModuleInterop": true, 22 | "moduleResolution": "node", 23 | "forceConsistentCasingInFileNames": true, 24 | "strict": true, 25 | "skipLibCheck": true 26 | }, 27 | "exclude": ["node_modules", "fixtures"] 28 | } 29 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/fixtures/fragment.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './graphql'; 2 | 3 | // prettier-ignore 4 | export const PokemonFields = graphql(` 5 | fragment pokemonFields on Pokemon { 6 | id 7 | name 8 | fleeRate 9 | 10 | } 11 | `); 12 | 13 | // prettier-ignore 14 | export const Regression190 = graphql(` 15 | fragment pokemonFields on Pokemon { 16 | id 17 | name 18 | fleeRate 19 | 20 | } 21 | `); 22 | 23 | export const Pokemon = () => {}; 24 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/fixtures/graphql.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | import type { introspection } from '../introspection'; 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: introspection; 6 | }>(); 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 | export { readFragment } from 'gql.tada'; 10 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/fixtures/simple.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './graphql'; 2 | import { PokemonFields } from './fragment'; 3 | 4 | // prettier-ignore 5 | const x = graphql(` 6 | query Pok($limit: Int!) { 7 | pokemons(limit: $limit) { 8 | id 9 | name 10 | fleeRate 11 | classification 12 | ...pokemonFields 13 | __typename 14 | } 15 | } 16 | `, [PokemonFields]); 17 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/fixtures/type-condition.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './graphql'; 2 | import { PokemonFields } from './fragment'; 3 | 4 | // prettier-ignore 5 | const x = graphql(` 6 | query Pok($limit: Int!) { 7 | pokemons(limit: $limit) { 8 | id 9 | name 10 | fleeRate 11 | classification 12 | ...pokemonFields 13 | __typename 14 | ... on 15 | } 16 | } 17 | `, [PokemonFields]); 18 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/fixtures/unused-fragment.ts: -------------------------------------------------------------------------------- 1 | import { graphql } from './graphql'; 2 | import { Pokemon } from './fragment'; 3 | 4 | const x = graphql(` 5 | query Pok($limit: Int!) { 6 | pokemons(limit: $limit) { 7 | id 8 | name 9 | } 10 | } 11 | `); 12 | 13 | console.log(Pokemon); 14 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/graphql.ts: -------------------------------------------------------------------------------- 1 | import { initGraphQLTada } from 'gql.tada'; 2 | import type { introspection } from './introspection'; 3 | 4 | export const graphql = initGraphQLTada<{ 5 | introspection: introspection; 6 | }>(); 7 | 8 | export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'; 9 | export { readFragment } from 'gql.tada'; 10 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/introspection.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | 4 | /** An IntrospectionQuery representation of your schema. 5 | * 6 | * @remarks 7 | * This is an introspection of your schema saved as a file by GraphQLSP. 8 | * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents. 9 | * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to 10 | * instead save to a .ts instead of a .d.ts file. 11 | */ 12 | export type introspection = { 13 | name: never; 14 | query: 'Query'; 15 | mutation: never; 16 | subscription: never; 17 | types: { 18 | 'Attack': { kind: 'OBJECT'; name: 'Attack'; fields: { 'damage': { name: 'damage'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'type': { name: 'type'; type: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; } }; }; }; 19 | 'AttacksConnection': { kind: 'OBJECT'; name: 'AttacksConnection'; fields: { 'fast': { name: 'fast'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; 'special': { name: 'special'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Attack'; ofType: null; }; } }; }; }; 20 | 'Boolean': unknown; 21 | 'EvolutionRequirement': { kind: 'OBJECT'; name: 'EvolutionRequirement'; fields: { 'amount': { name: 'amount'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 22 | 'Float': unknown; 23 | 'ID': unknown; 24 | 'Int': unknown; 25 | 'Pokemon': { kind: 'OBJECT'; name: 'Pokemon'; fields: { 'attacks': { name: 'attacks'; type: { kind: 'OBJECT'; name: 'AttacksConnection'; ofType: null; } }; 'classification': { name: 'classification'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'evolutionRequirements': { name: 'evolutionRequirements'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'EvolutionRequirement'; ofType: null; }; } }; 'evolutions': { name: 'evolutions'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; 'fleeRate': { name: 'fleeRate'; type: { kind: 'SCALAR'; name: 'Float'; ofType: null; } }; 'height': { name: 'height'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'maxCP': { name: 'maxCP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'maxHP': { name: 'maxHP'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'resistant': { name: 'resistant'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'types': { name: 'types'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weaknesses': { name: 'weaknesses'; type: { kind: 'LIST'; name: never; ofType: { kind: 'ENUM'; name: 'PokemonType'; ofType: null; }; } }; 'weight': { name: 'weight'; type: { kind: 'OBJECT'; name: 'PokemonDimension'; ofType: null; } }; }; }; 26 | 'PokemonDimension': { kind: 'OBJECT'; name: 'PokemonDimension'; fields: { 'maximum': { name: 'maximum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'minimum': { name: 'minimum'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 27 | 'PokemonType': { name: 'PokemonType'; enumValues: 'Bug' | 'Dark' | 'Dragon' | 'Electric' | 'Fairy' | 'Fighting' | 'Fire' | 'Flying' | 'Ghost' | 'Grass' | 'Ground' | 'Ice' | 'Normal' | 'Poison' | 'Psychic' | 'Rock' | 'Steel' | 'Water'; }; 28 | 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'pokemon': { name: 'pokemon'; type: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; } }; 'pokemons': { name: 'pokemons'; type: { kind: 'LIST'; name: never; ofType: { kind: 'OBJECT'; name: 'Pokemon'; ofType: null; }; } }; }; }; 29 | 'String': unknown; 30 | }; 31 | }; 32 | 33 | import * as gqlTada from 'gql.tada'; 34 | 35 | declare module 'gql.tada' { 36 | interface setupSchema { 37 | introspection: introspection; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "private": true, 4 | "dependencies": { 5 | "graphql": "^16.0.0", 6 | "gql.tada": "^1.6.0", 7 | "@graphql-typed-document-node/core": "^3.0.0", 8 | "@0no-co/graphqlsp": "workspace:*", 9 | "@urql/core": "^4.0.4" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^5.3.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /test/e2e/fixture-project-tada/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schema": "./schema.graphql", 7 | "tadaOutputLocation": "./introspection.d.ts" 8 | } 9 | ], 10 | "target": "es2016", 11 | "esModuleInterop": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "strict": true, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": ["node_modules", "fixtures"] 18 | } 19 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/__generated__/baseGraphQLSP.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null; 2 | export type InputMaybe = Maybe; 3 | export type Exact = { 4 | [K in keyof T]: T[K]; 5 | }; 6 | export type MakeOptional = Omit & { 7 | [SubKey in K]?: Maybe; 8 | }; 9 | export type MakeMaybe = Omit & { 10 | [SubKey in K]: Maybe; 11 | }; 12 | export type MakeEmpty< 13 | T extends { [key: string]: unknown }, 14 | K extends keyof T 15 | > = { [_ in K]?: never }; 16 | export type Incremental = 17 | | T 18 | | { 19 | [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; 20 | }; 21 | /** All built-in and custom scalars, mapped to their actual values */ 22 | export type Scalars = { 23 | ID: { input: string; output: string }; 24 | String: { input: string; output: string }; 25 | Boolean: { input: boolean; output: boolean }; 26 | Int: { input: number; output: number }; 27 | Float: { input: number; output: number }; 28 | }; 29 | 30 | /** Move a Pokémon can perform with the associated damage and type. */ 31 | export type Attack = { 32 | __typename: 'Attack'; 33 | damage?: Maybe; 34 | name?: Maybe; 35 | type?: Maybe; 36 | }; 37 | 38 | export type AttacksConnection = { 39 | __typename: 'AttacksConnection'; 40 | fast?: Maybe>>; 41 | special?: Maybe>>; 42 | }; 43 | 44 | /** Requirement that prevents an evolution through regular means of levelling up. */ 45 | export type EvolutionRequirement = { 46 | __typename: 'EvolutionRequirement'; 47 | amount?: Maybe; 48 | name?: Maybe; 49 | }; 50 | 51 | export type Pokemon = { 52 | __typename: 'Pokemon'; 53 | attacks?: Maybe; 54 | /** @deprecated And this is the reason why */ 55 | classification?: Maybe; 56 | evolutionRequirements?: Maybe>>; 57 | evolutions?: Maybe>>; 58 | /** Likelihood of an attempt to catch a Pokémon to fail. */ 59 | fleeRate?: Maybe; 60 | height?: Maybe; 61 | id: Scalars['ID']['output']; 62 | /** Maximum combat power a Pokémon may achieve at max level. */ 63 | maxCP?: Maybe; 64 | /** Maximum health points a Pokémon may achieve at max level. */ 65 | maxHP?: Maybe; 66 | name: Scalars['String']['output']; 67 | resistant?: Maybe>>; 68 | types?: Maybe>>; 69 | weaknesses?: Maybe>>; 70 | weight?: Maybe; 71 | }; 72 | 73 | export type PokemonDimension = { 74 | __typename: 'PokemonDimension'; 75 | maximum?: Maybe; 76 | minimum?: Maybe; 77 | }; 78 | 79 | /** Elemental property associated with either a Pokémon or one of their moves. */ 80 | export type PokemonType = 81 | | 'Bug' 82 | | 'Dark' 83 | | 'Dragon' 84 | | 'Electric' 85 | | 'Fairy' 86 | | 'Fighting' 87 | | 'Fire' 88 | | 'Flying' 89 | | 'Ghost' 90 | | 'Grass' 91 | | 'Ground' 92 | | 'Ice' 93 | | 'Normal' 94 | | 'Poison' 95 | | 'Psychic' 96 | | 'Rock' 97 | | 'Steel' 98 | | 'Water'; 99 | 100 | export type Query = { 101 | __typename: 'Query'; 102 | /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ 103 | pokemon?: Maybe; 104 | /** List out all Pokémon, optionally in pages */ 105 | pokemons?: Maybe>>; 106 | }; 107 | 108 | export type QueryPokemonArgs = { 109 | id: Scalars['ID']['input']; 110 | }; 111 | 112 | export type QueryPokemonsArgs = { 113 | limit?: InputMaybe; 114 | skip?: InputMaybe; 115 | }; 116 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/bail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useQuery } from 'urql'; 3 | import { graphql } from './gql'; 4 | // @ts-expect-error 5 | import { Pokemon } from './fragment'; 6 | 7 | const PokemonQuery = graphql(` 8 | query Po($id: ID!) { 9 | pokemon(id: $id) { 10 | id 11 | fleeRate 12 | ...pokemonFields 13 | attacks { 14 | special { 15 | name 16 | damage 17 | } 18 | } 19 | weight { 20 | minimum 21 | maximum 22 | } 23 | name 24 | __typename 25 | } 26 | } 27 | `); 28 | 29 | const Pokemons = () => { 30 | const [result] = useQuery({ 31 | query: PokemonQuery, 32 | variables: { id: '' } 33 | }); 34 | 35 | const pokemon = React.useMemo(() => result.data?.pokemon, []) 36 | 37 | // @ts-expect-error 38 | return ; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/chained-usage.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | import { useMemo } from 'react'; 3 | import { graphql } from './gql'; 4 | 5 | const PokemonsQuery = graphql( 6 | ` 7 | query Pok { 8 | pokemons { 9 | name 10 | maxCP 11 | maxHP 12 | fleeRate 13 | } 14 | } 15 | ` 16 | ); 17 | 18 | const Pokemons = () => { 19 | const [result] = useQuery({ 20 | query: PokemonsQuery, 21 | }); 22 | 23 | const results = useMemo(() => { 24 | if (!result.data?.pokemons) return []; 25 | return ( 26 | result.data.pokemons 27 | .filter(i => i?.name === 'Pikachu') 28 | .map(p => ({ 29 | x: p?.maxCP, 30 | y: p?.maxHP, 31 | })) ?? [] 32 | ); 33 | }, [result.data?.pokemons]); 34 | 35 | // @ts-ignore 36 | return results; 37 | }; 38 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/destructuring.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | import { graphql } from './gql'; 3 | // @ts-expect-error 4 | import { Pokemon } from './fragment'; 5 | import * as React from 'react'; 6 | 7 | const PokemonQuery = graphql(` 8 | query Po($id: ID!) { 9 | pokemon(id: $id) { 10 | id 11 | fleeRate 12 | ...pokemonFields 13 | attacks { 14 | special { 15 | name 16 | damage 17 | } 18 | } 19 | weight { 20 | minimum 21 | maximum 22 | } 23 | name 24 | __typename 25 | } 26 | } 27 | `); 28 | 29 | const Pokemons = () => { 30 | const [result] = useQuery({ 31 | query: PokemonQuery, 32 | variables: { id: '' } 33 | }); 34 | 35 | // Works 36 | const { fleeRate } = result.data?.pokemon || {}; 37 | console.log(fleeRate) 38 | // @ts-expect-error 39 | const { pokemon: { weight: { minimum } } } = result.data || {}; 40 | console.log(minimum) 41 | 42 | // Works 43 | const { pokemon } = result.data || {}; 44 | console.log(pokemon?.weight?.maximum) 45 | 46 | // @ts-expect-error 47 | return ; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/fragment-destructuring.tsx: -------------------------------------------------------------------------------- 1 | import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 2 | import { graphql } from './gql'; 3 | 4 | export const PokemonFields = graphql(` 5 | fragment pokemonFields on Pokemon { 6 | id 7 | name 8 | attacks { 9 | fast { 10 | damage 11 | name 12 | } 13 | } 14 | } 15 | `) 16 | 17 | export const Pokemon = (data: any) => { 18 | const { name } = useFragment(PokemonFields, data); 19 | return `hi ${name}`; 20 | }; 21 | 22 | export function useFragment( 23 | _fragment: TypedDocumentNode, 24 | data: any 25 | ): Type { 26 | return data; 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/fragment.tsx: -------------------------------------------------------------------------------- 1 | import { TypedDocumentNode } from '@graphql-typed-document-node/core'; 2 | import { graphql } from './gql'; 3 | 4 | export const PokemonFields = graphql(` 5 | fragment pokemonFields on Pokemon { 6 | id 7 | name 8 | attacks { 9 | fast { 10 | damage 11 | name 12 | } 13 | } 14 | } 15 | `) 16 | 17 | export const Pokemon = (data: any) => { 18 | const pokemon = useFragment(PokemonFields, data); 19 | return `hi ${pokemon.name}`; 20 | }; 21 | 22 | export function useFragment( 23 | _fragment: TypedDocumentNode, 24 | data: any 25 | ): Type { 26 | return data; 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResultOf, 3 | DocumentTypeDecoration, 4 | TypedDocumentNode, 5 | } from '@graphql-typed-document-node/core'; 6 | import { FragmentDefinitionNode } from 'graphql'; 7 | import { Incremental } from './graphql'; 8 | 9 | export type FragmentType< 10 | TDocumentType extends DocumentTypeDecoration 11 | > = TDocumentType extends DocumentTypeDecoration 12 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 13 | ? TKey extends string 14 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 15 | : never 16 | : never 17 | : never; 18 | 19 | // return non-nullable if `fragmentType` is non-nullable 20 | export function useFragment( 21 | _documentNode: DocumentTypeDecoration, 22 | fragmentType: FragmentType> 23 | ): TType; 24 | // return nullable if `fragmentType` is nullable 25 | export function useFragment( 26 | _documentNode: DocumentTypeDecoration, 27 | fragmentType: 28 | | FragmentType> 29 | | null 30 | | undefined 31 | ): TType | null | undefined; 32 | // return array of non-nullable if `fragmentType` is array of non-nullable 33 | export function useFragment( 34 | _documentNode: DocumentTypeDecoration, 35 | fragmentType: ReadonlyArray>> 36 | ): ReadonlyArray; 37 | // return array of nullable if `fragmentType` is array of nullable 38 | export function useFragment( 39 | _documentNode: DocumentTypeDecoration, 40 | fragmentType: 41 | | ReadonlyArray>> 42 | | null 43 | | undefined 44 | ): ReadonlyArray | null | undefined; 45 | export function useFragment( 46 | _documentNode: DocumentTypeDecoration, 47 | fragmentType: 48 | | FragmentType> 49 | | ReadonlyArray>> 50 | | null 51 | | undefined 52 | ): TType | ReadonlyArray | null | undefined { 53 | return fragmentType as any; 54 | } 55 | 56 | export function makeFragmentData< 57 | F extends DocumentTypeDecoration, 58 | FT extends ResultOf 59 | >(data: FT, _fragment: F): FragmentType { 60 | return data as FragmentType; 61 | } 62 | export function isFragmentReady( 63 | queryNode: DocumentTypeDecoration, 64 | fragmentNode: TypedDocumentNode, 65 | data: 66 | | FragmentType, any>> 67 | | null 68 | | undefined 69 | ): data is FragmentType { 70 | const deferredFields = ( 71 | queryNode as { 72 | __meta__?: { deferredFields: Record }; 73 | } 74 | ).__meta__?.deferredFields; 75 | 76 | if (!deferredFields) return true; 77 | 78 | const fragDef = fragmentNode.definitions[0] as 79 | | FragmentDefinitionNode 80 | | undefined; 81 | const fragName = fragDef?.name?.value; 82 | 83 | const fields = (fragName && deferredFields[fragName]) || []; 84 | return fields.length > 0 && fields.every(field => data && field in data); 85 | } 86 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/gql/gql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as types from './graphql'; 3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | 5 | /** 6 | * Map of all GraphQL operations in the project. 7 | * 8 | * This map has several performance disadvantages: 9 | * 1. It is not tree-shakeable, so it will include all operations in the project. 10 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 | * 3. It does not support dead code elimination, so it will add unused operations. 12 | * 13 | * Therefore it is highly recommended to use the babel or swc plugin for production. 14 | */ 15 | const documents = { 16 | '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 | types.PokemonFieldsFragmentDoc, 18 | '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n': 19 | types.PoDocument, 20 | '\n query Pok {\n pokemons {\n name\n maxCP\n maxHP\n fleeRate\n }\n }\n ': 21 | types.PokDocument, 22 | }; 23 | 24 | /** 25 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 26 | * 27 | * 28 | * @example 29 | * ```ts 30 | * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 31 | * ``` 32 | * 33 | * The query argument is unknown! 34 | * Please regenerate the types. 35 | */ 36 | export function graphql(source: string): unknown; 37 | 38 | /** 39 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 40 | */ 41 | export function graphql( 42 | source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n' 43 | ): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n']; 44 | /** 45 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 46 | */ 47 | export function graphql( 48 | source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n' 49 | ): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n']; 50 | /** 51 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 52 | */ 53 | export function graphql( 54 | source: '\n query Pok {\n pokemons {\n name\n maxCP\n maxHP\n fleeRate\n }\n }\n ' 55 | ): (typeof documents)['\n query Pok {\n pokemons {\n name\n maxCP\n maxHP\n fleeRate\n }\n }\n ']; 56 | 57 | export function graphql(source: string) { 58 | return (documents as any)[source] ?? {}; 59 | } 60 | 61 | export type DocumentType> = 62 | TDocumentNode extends DocumentNode ? TType : never; 63 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-masking'; 2 | export * from './gql'; 3 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/immediate-destructuring.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | import { graphql } from './gql'; 3 | // @ts-expect-error 4 | import { Pokemon } from './fragment'; 5 | import * as React from 'react'; 6 | 7 | const PokemonQuery = graphql(` 8 | query Po($id: ID!) { 9 | pokemon(id: $id) { 10 | id 11 | fleeRate 12 | ...pokemonFields 13 | attacks { 14 | special { 15 | name 16 | damage 17 | } 18 | } 19 | weight { 20 | minimum 21 | maximum 22 | } 23 | name 24 | __typename 25 | } 26 | } 27 | `); 28 | 29 | const Pokemons = () => { 30 | // @ts-expect-error 31 | const [{ data: { pokemon: { fleeRate, weight: { minimum, maximum } } } }] = useQuery({ 32 | query: PokemonQuery, 33 | variables: { id: '' } 34 | }); 35 | 36 | // @ts-expect-error 37 | return ; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/fixtures/property-access.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'urql'; 2 | import { graphql } from './gql'; 3 | // @ts-expect-error 4 | import { Pokemon } from './fragment'; 5 | import * as React from 'react'; 6 | 7 | const PokemonQuery = graphql(` 8 | query Po($id: ID!) { 9 | pokemon(id: $id) { 10 | id 11 | fleeRate 12 | ...pokemonFields 13 | attacks { 14 | special { 15 | name 16 | damage 17 | } 18 | } 19 | weight { 20 | minimum 21 | maximum 22 | } 23 | name 24 | __typename 25 | } 26 | } 27 | `); 28 | 29 | const Pokemons = () => { 30 | const [result] = useQuery({ 31 | query: PokemonQuery, 32 | variables: { id: '' } 33 | }); 34 | 35 | const pokemon = result.data?.pokemon 36 | console.log(result.data?.pokemon?.attacks && result.data?.pokemon?.attacks.special && result.data?.pokemon?.attacks.special[0] && result.data?.pokemon?.attacks.special[0].name) 37 | console.log(pokemon?.name) 38 | 39 | // @ts-expect-error 40 | return ; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResultOf, 3 | DocumentTypeDecoration, 4 | TypedDocumentNode, 5 | } from '@graphql-typed-document-node/core'; 6 | import { FragmentDefinitionNode } from 'graphql'; 7 | import { Incremental } from './graphql'; 8 | 9 | export type FragmentType< 10 | TDocumentType extends DocumentTypeDecoration 11 | > = TDocumentType extends DocumentTypeDecoration 12 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 13 | ? TKey extends string 14 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 15 | : never 16 | : never 17 | : never; 18 | 19 | // return non-nullable if `fragmentType` is non-nullable 20 | export function useFragment( 21 | _documentNode: DocumentTypeDecoration, 22 | fragmentType: FragmentType> 23 | ): TType; 24 | // return nullable if `fragmentType` is nullable 25 | export function useFragment( 26 | _documentNode: DocumentTypeDecoration, 27 | fragmentType: 28 | | FragmentType> 29 | | null 30 | | undefined 31 | ): TType | null | undefined; 32 | // return array of non-nullable if `fragmentType` is array of non-nullable 33 | export function useFragment( 34 | _documentNode: DocumentTypeDecoration, 35 | fragmentType: ReadonlyArray>> 36 | ): ReadonlyArray; 37 | // return array of nullable if `fragmentType` is array of nullable 38 | export function useFragment( 39 | _documentNode: DocumentTypeDecoration, 40 | fragmentType: 41 | | ReadonlyArray>> 42 | | null 43 | | undefined 44 | ): ReadonlyArray | null | undefined; 45 | export function useFragment( 46 | _documentNode: DocumentTypeDecoration, 47 | fragmentType: 48 | | FragmentType> 49 | | ReadonlyArray>> 50 | | null 51 | | undefined 52 | ): TType | ReadonlyArray | null | undefined { 53 | return fragmentType as any; 54 | } 55 | 56 | export function makeFragmentData< 57 | F extends DocumentTypeDecoration, 58 | FT extends ResultOf 59 | >(data: FT, _fragment: F): FragmentType { 60 | return data as FragmentType; 61 | } 62 | export function isFragmentReady( 63 | queryNode: DocumentTypeDecoration, 64 | fragmentNode: TypedDocumentNode, 65 | data: 66 | | FragmentType, any>> 67 | | null 68 | | undefined 69 | ): data is FragmentType { 70 | const deferredFields = ( 71 | queryNode as { 72 | __meta__?: { deferredFields: Record }; 73 | } 74 | ).__meta__?.deferredFields; 75 | 76 | if (!deferredFields) return true; 77 | 78 | const fragDef = fragmentNode.definitions[0] as 79 | | FragmentDefinitionNode 80 | | undefined; 81 | const fragName = fragDef?.name?.value; 82 | 83 | const fields = (fragName && deferredFields[fragName]) || []; 84 | return fields.length > 0 && fields.every(field => data && field in data); 85 | } 86 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/gql/gql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as types from './graphql'; 3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | 5 | /** 6 | * Map of all GraphQL operations in the project. 7 | * 8 | * This map has several performance disadvantages: 9 | * 1. It is not tree-shakeable, so it will include all operations in the project. 10 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 | * 3. It does not support dead code elimination, so it will add unused operations. 12 | * 13 | * Therefore it is highly recommended to use the babel or swc plugin for production. 14 | */ 15 | const documents = { 16 | '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n': 17 | types.PokemonFieldsFragmentDoc, 18 | '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n': 19 | types.PoDocument, 20 | }; 21 | 22 | /** 23 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 24 | * 25 | * 26 | * @example 27 | * ```ts 28 | * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 29 | * ``` 30 | * 31 | * The query argument is unknown! 32 | * Please regenerate the types. 33 | */ 34 | export function graphql(source: string): unknown; 35 | 36 | /** 37 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 38 | */ 39 | export function graphql( 40 | source: '\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n' 41 | ): (typeof documents)['\n fragment pokemonFields on Pokemon {\n id\n name\n attacks {\n fast {\n damage\n name\n }\n }\n }\n']; 42 | /** 43 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 44 | */ 45 | export function graphql( 46 | source: '\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n' 47 | ): (typeof documents)['\n query Po($id: ID!) {\n pokemon(id: $id) {\n id\n fleeRate\n ...pokemonFields\n attacks {\n special {\n name\n damage\n }\n }\n weight {\n minimum\n maximum\n }\n name\n __typename\n }\n }\n']; 48 | 49 | export function graphql(source: string) { 50 | return (documents as any)[source] ?? {}; 51 | } 52 | 53 | export type DocumentType> = 54 | TDocumentNode extends DocumentNode ? TType : never; 55 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-masking'; 2 | export * from './gql'; 3 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "private": true, 4 | "dependencies": { 5 | "graphql": "^16.0.0", 6 | "@graphql-typed-document-node/core": "^3.0.0", 7 | "@0no-co/graphqlsp": "workspace:*", 8 | "@urql/core": "^4.0.4", 9 | "urql": "^4.0.4" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "18.2.45", 13 | "typescript": "^5.3.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/schema.graphql: -------------------------------------------------------------------------------- 1 | ### This file was generated by Nexus Schema 2 | ### Do not make changes to this file directly 3 | 4 | """ 5 | Move a Pokémon can perform with the associated damage and type. 6 | """ 7 | type Attack { 8 | damage: Int 9 | name: String 10 | type: PokemonType 11 | } 12 | 13 | type AttacksConnection { 14 | fast: [Attack] 15 | special: [Attack] 16 | } 17 | 18 | """ 19 | Requirement that prevents an evolution through regular means of levelling up. 20 | """ 21 | type EvolutionRequirement { 22 | amount: Int 23 | name: String 24 | } 25 | 26 | type Pokemon { 27 | attacks: AttacksConnection 28 | classification: String @deprecated(reason: "And this is the reason why") 29 | evolutionRequirements: [EvolutionRequirement] 30 | evolutions: [Pokemon] 31 | 32 | """ 33 | Likelihood of an attempt to catch a Pokémon to fail. 34 | """ 35 | fleeRate: Float 36 | height: PokemonDimension 37 | id: ID! 38 | 39 | """ 40 | Maximum combat power a Pokémon may achieve at max level. 41 | """ 42 | maxCP: Int 43 | 44 | """ 45 | Maximum health points a Pokémon may achieve at max level. 46 | """ 47 | maxHP: Int 48 | name: String! 49 | resistant: [PokemonType] 50 | types: [PokemonType] 51 | weaknesses: [PokemonType] 52 | weight: PokemonDimension 53 | } 54 | 55 | type PokemonDimension { 56 | maximum: String 57 | minimum: String 58 | } 59 | 60 | """ 61 | Elemental property associated with either a Pokémon or one of their moves. 62 | """ 63 | enum PokemonType { 64 | Bug 65 | Dark 66 | Dragon 67 | Electric 68 | Fairy 69 | Fighting 70 | Fire 71 | Flying 72 | Ghost 73 | Grass 74 | Ground 75 | Ice 76 | Normal 77 | Poison 78 | Psychic 79 | Rock 80 | Steel 81 | Water 82 | } 83 | 84 | type Query { 85 | """ 86 | Get a single Pokémon by its ID, a three character long identifier padded with zeroes 87 | """ 88 | pokemon(id: ID!): Pokemon 89 | 90 | """ 91 | List out all Pokémon, optionally in pages 92 | """ 93 | pokemons(limit: Int, skip: Int): [Pokemon] 94 | } -------------------------------------------------------------------------------- /test/e2e/fixture-project-unused-fields/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schema": "./schema.graphql", 7 | "disableTypegen": true, 8 | "trackFieldUsage": true, 9 | "shouldCheckForColocatedFragments": false 10 | } 11 | ], 12 | "target": "es2016", 13 | "jsx": "react-jsx", 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "forceConsistentCasingInFileNames": true, 17 | "strict": true, 18 | "skipLibCheck": true 19 | }, 20 | "exclude": ["node_modules", "fixtures"] 21 | } 22 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/fixtures/Combination.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@urql/core"; 2 | 3 | const frag = gql` 4 | fragment fields on Post { 5 | id 6 | someUnknownField 7 | } 8 | `; 9 | 10 | const query = gql` 11 | ${frag} 12 | 13 | query Po { 14 | posts { 15 | 16 | __typenam 17 | ...fields 18 | } 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/fixtures/Post.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@urql/core"; 2 | 3 | export const PostFields = gql` 4 | fragment postFields on Post { 5 | title 6 | } 7 | ` 8 | 9 | export const Post = (post: any) => { 10 | return post.title 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/fixtures/Posts.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@urql/core"; 2 | import { Post } from "./Post"; 3 | 4 | const PostsQuery = gql` 5 | query PostsList { 6 | posts { 7 | id 8 | } 9 | } 10 | ` 11 | 12 | Post({ title: '' }) 13 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/fixtures/rename-complex.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@urql/core'; 2 | 3 | export const PostFields = gql` 4 | fragment PostFields on Post { 5 | id 6 | } 7 | ` as typeof import('./rename-complex.generated').PostFieldsFragmentDoc; 8 | 9 | export const Post2Fields = gql` 10 | fragment Post2Fields on Post { 11 | title 12 | } 13 | ` as typeof import('./rename-complex.generated').PostFieldsFragmentDoc; 14 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/fixtures/rename.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@urql/core'; 2 | 3 | const PostsQuery = gql` 4 | query Posts { 5 | posts { 6 | title 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/fixtures/simple.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@urql/core'; 2 | 3 | const PostsQuery = gql` 4 | query AllPosts { 5 | posts { 6 | title 7 | 8 | } 9 | } 10 | `; 11 | 12 | const Regression190 = gql` 13 | query AllPosts { 14 | 15 | } 16 | `; 17 | 18 | const sql = (x: string | TemplateStringsArray) => x; 19 | const x = sql`'{}'`; 20 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "private": true, 4 | "dependencies": { 5 | "@0no-co/graphqlsp": "workspace:*", 6 | "@urql/core": "^4.0.4" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | post(id: ID!): Post 3 | """ 4 | List out all posts 5 | """ 6 | posts: [Post] 7 | } 8 | 9 | type Post { 10 | id: ID! 11 | title: String! 12 | content: String! 13 | } 14 | -------------------------------------------------------------------------------- /test/e2e/fixture-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "@0no-co/graphqlsp", 6 | "schema": "./schema.graphql", 7 | "templateIsCallExpression": false 8 | } 9 | ], 10 | "target": "es2016", 11 | "esModuleInterop": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "strict": true, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": ["node_modules", "fixtures"] 18 | } 19 | -------------------------------------------------------------------------------- /test/e2e/graphqlsp.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, afterAll, beforeAll, it, describe } from 'vitest'; 2 | import { TSServer } from './server'; 3 | import path from 'node:path'; 4 | import fs from 'node:fs'; 5 | import url from 'node:url'; 6 | import ts from 'typescript/lib/tsserverlibrary'; 7 | 8 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 9 | 10 | const projectPath = path.resolve(__dirname, 'fixture-project'); 11 | 12 | let server: TSServer; 13 | 14 | describe('simple', () => { 15 | const testFile = path.join(projectPath, 'simple.ts'); 16 | const generatedFile = path.join(projectPath, 'simple.generated.ts'); 17 | const baseGeneratedFile = path.join( 18 | projectPath, 19 | '__generated__/baseGraphQLSP.ts' 20 | ); 21 | 22 | beforeAll(async () => { 23 | server = new TSServer(projectPath, { debugLog: false }); 24 | const fixtureFileContent = fs.readFileSync( 25 | path.resolve(testFile, '../fixtures/simple.ts'), 26 | 'utf-8' 27 | ); 28 | 29 | server.sendCommand('open', { 30 | file: testFile, 31 | fileContent: '// empty', 32 | scriptKindName: 'TS', 33 | } satisfies ts.server.protocol.OpenRequestArgs); 34 | 35 | server.sendCommand('updateOpen', { 36 | openFiles: [{ file: testFile, fileContent: fixtureFileContent }], 37 | } satisfies ts.server.protocol.UpdateOpenRequestArgs); 38 | 39 | server.sendCommand('saveto', { 40 | file: testFile, 41 | tmpfile: testFile, 42 | } satisfies ts.server.protocol.SavetoRequestArgs); 43 | 44 | await server.waitForResponse( 45 | response => response.type === 'event' && response.event === 'setTypings' 46 | ); 47 | }); 48 | 49 | afterAll(() => { 50 | try { 51 | fs.unlinkSync(testFile); 52 | fs.unlinkSync(generatedFile); 53 | fs.unlinkSync(baseGeneratedFile); 54 | } catch {} 55 | server.close(); 56 | }); 57 | 58 | it('Proposes suggestions for a selection-set', async () => { 59 | server.send({ 60 | seq: 8, 61 | type: 'request', 62 | command: 'completionInfo', 63 | arguments: { 64 | file: testFile, 65 | line: 7, 66 | offset: 7, 67 | includeExternalModuleExports: true, 68 | includeInsertTextCompletions: true, 69 | triggerKind: 1, 70 | }, 71 | }); 72 | 73 | await server.waitForResponse( 74 | response => 75 | response.type === 'response' && response.command === 'completionInfo' 76 | ); 77 | 78 | const res = server.responses 79 | .reverse() 80 | .find( 81 | resp => resp.type === 'response' && resp.command === 'completionInfo' 82 | ); 83 | 84 | expect(res).toBeDefined(); 85 | expect(typeof res?.body.entries).toEqual('object'); 86 | const defaultAttrs = { kind: 'var', kindModifiers: 'declare' }; 87 | expect(res?.body.entries).toEqual([ 88 | { 89 | ...defaultAttrs, 90 | name: 'id', 91 | sortText: '0id', 92 | labelDetails: { detail: ' ID!' }, 93 | }, 94 | { 95 | ...defaultAttrs, 96 | name: 'content', 97 | sortText: '2content', 98 | labelDetails: { detail: ' String!' }, 99 | }, 100 | { 101 | ...defaultAttrs, 102 | name: '__typename', 103 | sortText: '3__typename', 104 | labelDetails: { 105 | detail: ' String!', 106 | description: 'The name of the current Object type at runtime.', 107 | }, 108 | }, 109 | ]); 110 | }, 7500); 111 | 112 | it('Gives quick-info when hovering start (#15)', async () => { 113 | server.send({ 114 | seq: 9, 115 | type: 'request', 116 | command: 'quickinfo', 117 | arguments: { 118 | file: testFile, 119 | line: 5, 120 | offset: 5, 121 | }, 122 | }); 123 | 124 | await server.waitForResponse( 125 | response => 126 | response.type === 'response' && response.command === 'quickinfo' 127 | ); 128 | 129 | const res = server.responses 130 | .reverse() 131 | .find(resp => resp.type === 'response' && resp.command === 'quickinfo'); 132 | expect(res).toBeDefined(); 133 | expect(typeof res?.body).toEqual('object'); 134 | expect(res?.body.documentation).toEqual( 135 | `Query.posts: [Post]\n\nList out all posts` 136 | ); 137 | }, 7500); 138 | 139 | it('Handles empty line (#190)', async () => { 140 | server.send({ 141 | seq: 10, 142 | type: 'request', 143 | command: 'completionInfo', 144 | arguments: { 145 | file: testFile, 146 | line: 14, 147 | offset: 3, 148 | includeExternalModuleExports: true, 149 | includeInsertTextCompletions: true, 150 | triggerKind: 1, 151 | }, 152 | }); 153 | 154 | await server.waitForResponse( 155 | response => 156 | response.type === 'response' && response.command === 'completionInfo' 157 | ); 158 | 159 | const res = server.responses 160 | .reverse() 161 | .find( 162 | resp => resp.type === 'response' && resp.command === 'completionInfo' 163 | ); 164 | 165 | expect(res).toBeDefined(); 166 | expect(typeof res?.body.entries).toEqual('object'); 167 | const defaultAttrs = { kind: 'var', kindModifiers: 'declare' }; 168 | expect(res?.body.entries).toEqual([ 169 | { 170 | ...defaultAttrs, 171 | name: 'post', 172 | sortText: '0post', 173 | labelDetails: { detail: ' Post' }, 174 | }, 175 | { 176 | ...defaultAttrs, 177 | name: 'posts', 178 | sortText: '1posts', 179 | labelDetails: { detail: ' [Post]', description: 'List out all posts' }, 180 | }, 181 | { 182 | ...defaultAttrs, 183 | name: '__typename', 184 | sortText: '2__typename', 185 | labelDetails: { 186 | detail: ' String!', 187 | description: 'The name of the current Object type at runtime.', 188 | }, 189 | }, 190 | { 191 | ...defaultAttrs, 192 | name: '__schema', 193 | sortText: '3__schema', 194 | labelDetails: { 195 | detail: ' __Schema!', 196 | description: 'Access the current type schema of this server.', 197 | }, 198 | }, 199 | { 200 | ...defaultAttrs, 201 | name: '__type', 202 | sortText: '4__type', 203 | labelDetails: { 204 | detail: ' __Type', 205 | description: 'Request the type information of a single type.', 206 | }, 207 | }, 208 | ]); 209 | }, 7500); 210 | }); 211 | -------------------------------------------------------------------------------- /test/e2e/server.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, fork } from 'node:child_process'; 2 | import readline from 'node:readline'; 3 | import ts from 'typescript/lib/tsserverlibrary'; 4 | 5 | type Command = `${ts.server.protocol.CommandTypes}`; 6 | 7 | export class TSServer { 8 | #server: ChildProcess; 9 | #seq = 0; 10 | #resolvePromise: (() => void) | undefined; 11 | #waitFor: 12 | | (( 13 | response: ts.server.protocol.Response | ts.server.protocol.Event 14 | ) => boolean) 15 | | undefined; 16 | 17 | responses: Array = []; 18 | 19 | constructor( 20 | public projectPath: string, 21 | public options: { debugLog?: boolean } = {} 22 | ) { 23 | const tsserverPath = require.resolve('typescript/lib/tsserver'); 24 | 25 | const server = fork(tsserverPath, ['--logVerbosity', 'verbose'], { 26 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'], 27 | cwd: projectPath, 28 | env: { 29 | TSS_LOG: 30 | '-level verbose -traceToConsole false -logToFile true -file ./tsserver.log', 31 | }, 32 | }); 33 | 34 | if (!server?.stdout) { 35 | throw new Error('Failed to start tsserver'); 36 | } 37 | 38 | server.stdout.setEncoding('utf-8'); 39 | readline.createInterface({ input: server.stdout }).on('line', line => { 40 | if (!line.startsWith('{')) return; 41 | 42 | try { 43 | const data = JSON.parse(line); 44 | 45 | this.responses.push(data); 46 | 47 | if (this.#resolvePromise && this.#waitFor?.(data)) { 48 | this.#resolvePromise(); 49 | this.#waitFor = undefined; 50 | this.#resolvePromise = undefined; 51 | } 52 | 53 | if (options.debugLog) { 54 | console.log(data); 55 | } 56 | } catch (e) { 57 | console.error(e); 58 | } 59 | }); 60 | 61 | this.#server = server; 62 | } 63 | 64 | sendCommand(command: Command, args?: Record) { 65 | this.send({ command, arguments: args }); 66 | } 67 | 68 | send(data: {}) { 69 | const request = JSON.stringify({ 70 | seq: ++this.#seq, 71 | type: 'request', 72 | ...data, 73 | }); 74 | 75 | this.#server.stdin?.write(`${request}\n`); 76 | } 77 | 78 | waitForResponse = ( 79 | cb: ( 80 | response: ts.server.protocol.Response | ts.server.protocol.Event 81 | ) => boolean 82 | ) => { 83 | this.#waitFor = cb; 84 | return new Promise(resolve => { 85 | this.#resolvePromise = resolve; 86 | }); 87 | }; 88 | 89 | close() { 90 | this.#server.kill(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "noEmit": true, 6 | "outDir": "dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/util.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | type WaitForExpectOptions = { 4 | timeout?: number; 5 | interval?: number; 6 | }; 7 | 8 | export const waitForExpect = async ( 9 | expectFn: () => void, 10 | { interval = 2000, timeout = 30000 }: WaitForExpectOptions = {} 11 | ) => { 12 | // @sinonjs/fake-timers injects `clock` property into setTimeout 13 | const usesFakeTimers = 'clock' in setTimeout; 14 | 15 | if (usesFakeTimers) vi.useRealTimers(); 16 | 17 | const start = Date.now(); 18 | 19 | while (true) { 20 | try { 21 | expectFn(); 22 | break; 23 | } catch {} 24 | 25 | if (Date.now() - start > timeout) { 26 | throw new Error('Timeout'); 27 | } 28 | 29 | await new Promise(resolve => setTimeout(resolve, interval)); 30 | } 31 | 32 | if (usesFakeTimers) vi.useFakeTimers(); 33 | }; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | /* Language and Environment */ 5 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 6 | /* Modules */ 7 | "module": "commonjs" /* Specify what module code is generated. */, 8 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 9 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 10 | /* Type Checking */ 11 | "strict": true /* Enable all strict type-checking options. */, 12 | "noUncheckedIndexedAccess": true, 13 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 14 | } 15 | } 16 | --------------------------------------------------------------------------------