├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── actions │ └── ci-setup │ │ └── action.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.json ├── docs ├── no-transform.md └── using-ts-gql-with-apollo.md ├── package.json ├── packages ├── apollo │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.d.ts │ │ └── index.js ├── babel-plugin │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ └── index.ts ├── compiler │ ├── CHANGELOG.md │ ├── bin.js │ ├── cli │ │ └── package.json │ ├── package.json │ └── src │ │ ├── build.ts │ │ ├── cli.ts │ │ ├── extract-documents.ts │ │ ├── fs-operations.ts │ │ ├── fs.ts │ │ ├── get-documents.ts │ │ ├── get-generated-types.ts │ │ ├── index.ts │ │ ├── inline-fragments.tsx │ │ ├── integrity.ts │ │ ├── introspection-result.ts │ │ ├── operation-types.ts │ │ ├── schema-types.ts │ │ ├── schema.test.ts │ │ ├── schema.ts │ │ ├── test │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── index.test.ts │ │ ├── inline-fragments.test.tsx │ │ └── test-schema.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── validate-documents.ts │ │ ├── vendor │ │ ├── README.md │ │ ├── auto-bind.ts │ │ ├── codegen-core │ │ │ ├── execute-plugin.ts │ │ │ └── index.ts │ │ ├── typescript-operations │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── ts-operation-variables-to-object.ts │ │ │ ├── ts-selection-set-processor.ts │ │ │ └── visitor.ts │ │ └── visitor-plugin-common │ │ │ ├── avoid-optionals.ts │ │ │ ├── base-documents-visitor.ts │ │ │ ├── base-visitor.ts │ │ │ ├── declaration-kinds.ts │ │ │ ├── imports.ts │ │ │ ├── index.ts │ │ │ ├── mappers.ts │ │ │ ├── naming.ts │ │ │ ├── scalars.ts │ │ │ ├── selection-set-processor │ │ │ ├── base.ts │ │ │ └── pre-resolve-types.ts │ │ │ ├── selection-set-to-object.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── variables-to-object.ts │ │ ├── watch.ts │ │ ├── watcher.ts │ │ └── weakMemoize.ts ├── config │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── parse-schema.ts ├── eslint-plugin │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── __fixtures__ │ │ ├── allow-no-transform.mixed.ts │ │ ├── allow-transform.mixed.ts │ │ ├── basic.ts │ │ ├── disallow-interpolation-with-transform.ts │ │ ├── disallow-no-transform.ts │ │ ├── disallow-transform.no-transform.ts │ │ ├── incorrect-fragment-interpolate-num.no-transform.ts │ │ ├── interface.ts │ │ ├── missing-id.ts │ │ ├── missing-variable-with-another-variable-with-comma-after.ts │ │ ├── missing-variable-with-another-variable.ts │ │ ├── missing-variable.ts │ │ ├── multiple-validation-errors.ts │ │ ├── no-name.ts │ │ ├── parse-error.ts │ │ ├── union.ts │ │ ├── unknown-variable-type.ts │ │ ├── unused-variable.ts │ │ ├── with-array-destructuring-that-ignores.ts │ │ └── wrong-variable-type.ts │ │ ├── __snapshots__ │ │ └── test.ts.snap │ │ ├── get-nodes.ts │ │ ├── index.ts │ │ ├── parse.ts │ │ ├── test.ts │ │ └── utils.ts ├── fetch │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── next │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── schema │ └── README.md └── tag │ ├── CHANGELOG.md │ ├── no-transform │ └── package.json │ ├── package.json │ └── src │ ├── index.d.ts │ ├── index.js │ ├── no-transform.d.ts │ └── no-transform.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── test-app ├── .eslintrc.js ├── CHANGELOG.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── apollo.tsx │ └── thing │ │ └── thing.tsx ├── schema.graphql └── tsconfig.json ├── tsconfig.json └── types └── fixturez.d.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.0.3/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "Thinkmill/ts-gql" 7 | } 8 | ], 9 | "commit": false, 10 | "linked": [], 11 | "access": "public", 12 | "baseBranch": "main" 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | **/__generated__/ts-gql 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["plugin:@typescript-eslint/recommended"], 3 | parser: "@typescript-eslint/parser", 4 | rules: { 5 | "@typescript-eslint/explicit-function-return-type": 0, 6 | "@typescript-eslint/ban-ts-ignore": 0, 7 | "@typescript-eslint/no-explicit-any": 0, 8 | "@typescript-eslint/no-empty-function": 0, 9 | "prefer-const": 0, 10 | "@typescript-eslint/no-non-null-assertion": 0, 11 | "@typescript-eslint/no-empty-interface": 0, 12 | "@typescript-eslint/no-use-before-define": 0, 13 | "@typescript-eslint/no-var-requires": 0, 14 | "@typescript-eslint/ban-types": 0, 15 | "@typescript-eslint/explicit-module-boundary-types": 0, 16 | "@typescript-eslint/ban-ts-comment": 0, 17 | }, 18 | overrides: [ 19 | { 20 | files: ["index.test-d.ts"], 21 | rules: { 22 | "@typescript-eslint/no-unused-vars": 0, 23 | }, 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.github/actions/ci-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: "CI setup" 2 | runs: 3 | using: "composite" 4 | steps: 5 | - uses: actions/checkout@v2 6 | 7 | - run: npm i -g corepack@0.31.0 8 | shell: bash 9 | 10 | - name: Enable Corepack 11 | run: corepack enable 12 | shell: bash 13 | 14 | - name: Use Node.js 22 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 22 18 | cache: "pnpm" 19 | 20 | - name: Install Dependencies 21 | run: pnpm install 22 | shell: bash 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | typescript: 11 | name: TypeScript 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@main 15 | 16 | - uses: ./.github/actions/ci-setup 17 | 18 | - name: Check Types 19 | run: pnpm run types 20 | tests: 21 | name: "Tests on ${{matrix.platform}}" 22 | strategy: 23 | fail-fast: true 24 | matrix: 25 | platform: 26 | - ubuntu-latest 27 | # - windows-latest 28 | runs-on: ${{matrix.platform}} 29 | steps: 30 | - uses: actions/checkout@main 31 | - uses: ./.github/actions/ci-setup 32 | 33 | - name: Run Tests 34 | run: pnpm run test 35 | linting: 36 | name: Linting 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@main 40 | - uses: ./.github/actions/ci-setup 41 | 42 | - name: ESLint 43 | run: pnpm run lint 44 | - name: Prettier 45 | run: pnpm run format:check 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@main 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: ./.github/actions/ci-setup 19 | 20 | - name: "Create Pull Request or Publish to npm" 21 | uses: changesets/action@v1 22 | with: 23 | publish: pnpm run release 24 | version: pnpm run version 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | *.log 3 | .next 4 | dist 5 | __generated__ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-workspace-protocol=false 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | **/__generated__/ts-gql 4 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Thinkmill Labs Pty Ltd 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-gql 2 | 3 | > Write GraphQL queries with a gql tag in TypeScript -> have generated types 4 | 5 | ## Why? 6 | 7 | There are lots of great tools(some of which ts-gql uses internally!) for generating TypeScript types from GraphQL queries though a lot of the solutions have at least one of two problems: 8 | 9 | - The writing of a query isn't connected to the type that it results in 10 | - You're forced to write queries in `.graphql` files rather than inline 11 | - This also often means that you can't use fragments or that there's a new import syntax to learn 12 | 13 | ### How does ts-gql solve these problems? 14 | 15 | When using ts-gql, you write GraphQL queries with a tagged template literal like normal. 16 | 17 | ```tsx 18 | import { gql } from "@ts-gql/tag"; 19 | 20 | let myQuery = gql` 21 | query MyQuery { 22 | hello 23 | } 24 | `; 25 | ``` 26 | 27 | And then our ESLint plugin will auto-fix it to 28 | 29 | ```tsx 30 | import { gql } from "@ts-gql/tag"; 31 | 32 | let myQuery = gql` 33 | query MyQuery { 34 | hello 35 | } 36 | ` as import("../__generated__/ts-gql/MyQuery.ts").type; 37 | ``` 38 | 39 | You'll have the best experience if you have ESLint auto fix on save enabled in your editor. 40 | 41 |
42 | 43 | Why do we need to add `as import("__generated__/ts-gql/MyQuery.ts").type`? 44 | 45 | TypeScript doesn't currently type tagged template literals with literal string types so there is no way to get the correct type based on the call so we have to add `as import("__generated__/ts-gql/MyQuery.ts").type` though there are [issues](https://github.com/microsoft/TypeScript/issues/16552) [discussing](https://github.com/microsoft/TypeScript/issues/31422) [it](https://github.com/microsoft/TypeScript/issues/33304) which would remove the need for this. 46 | 47 |
48 | 49 | You can then use GraphQL Clients like Apollo Client, urql or any other GraphQL client that supports `@graphql-typed-document-node/core` and get types for the variables and data. 50 | 51 | ## Getting Started 52 | 53 | When using ts-gql, you'll need `@ts-gql/tag`, `@ts-gql/eslint-plugin` and `@ts-gql/compiler`. 54 | 55 | ```bash 56 | npm install graphql @ts-gql/tag @ts-gql/eslint-plugin @ts-gql/compiler 57 | ``` 58 | 59 | > If you're not already using [ESLint](https://eslint.org/) and [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint), you'll need those too. 60 | 61 | You'll need to add the ESLint plugin to your config and enable the `@ts-gql/ts-gql` rule. Your config might look something like this: 62 | 63 | ```json 64 | { 65 | "parser": "@typescript-eslint/parser", 66 | "plugins": ["@ts-gql"], 67 | "rules": { 68 | "@ts-gql/ts-gql": "error" 69 | } 70 | } 71 | ``` 72 | 73 | You now need to tell ts-gql where your GraphQL SDL file or introspection query result is. To do this, add to your `package.json`. Replace `schema.graphql` with the path to your SDL or introspection query result. 74 | 75 | ```json 76 | { 77 | "ts-gql": { 78 | "schema": "schema.graphql" 79 | } 80 | } 81 | ``` 82 | 83 | Add a script to your package.json and run it. You should run this in `postinstall` so that the types are generated when install happens. 84 | 85 | ```json 86 | { 87 | "scripts": { 88 | "postinstall": "ts-gql build", 89 | "ts-gql:build": "ts-gql build", 90 | "ts-gql:watch": "ts-gql watch" 91 | } 92 | } 93 | ``` 94 | 95 | You can run `npm run ts-gql:build` to do a single build or `npm run ts-gql:watch` to start watching. 96 | 97 | If you're using [Next.js](https://nextjs.org/), you can use `@ts-gql/next` to automatically start ts-gql's watcher when you start Next.js's dev server. 98 | 99 | ## Using Apollo 100 | 101 | See [docs/using-ts-gql-with-apollo.md](docs/using-ts-gql-with-apollo.md) for how to use ts-gql with Apollo. 102 | 103 | ## FAQ 104 | 105 | ### Why not let people interpolate fragments so you don't have to have unique fragment/operation names? 106 | 107 | This was the original plan! It's been abandoned though for a couple reasons: 108 | 109 | - Build time performance 110 | - Requiring type checking before you can generate types makes things _much_ slower 111 | - This is compounded by the fact that you not only have to do type checking but you have to do type checking n times where n is the maximum fragment depth. Because we want to encourage the use of fragments, a tool that gets significantly slower as you use more fragments would make it impractical to use fragments 112 | - Having to either not have Prettier work for documents with fragment interpolations(because Prettier will not format interpolations in GraphQL documents unless they are outside of the content of the document so you couldn't interpolate a fragment where it's used) or still have to name it and then you'd still have the potential for name conflicts(unless you added a syntax for renaming fragments at which point, I would say that that's way more complexity without a substantial gain) 113 | - Not allowing interpolation makes it very very clear what you can and can't do. Even if we allowed interpolations, they would be constrained which is a very difficult thing to explain. 114 | - Apollo already doesn't allow non-unique names anyway for their tooling anyway 115 | - TODO: there are more reasons 116 | 117 | ### How do you pass types around? 118 | 119 | `@ts-gql/tag` comes with an `OperationData` type to get the type of an operation(`mutation` or `query`) and `FragmentData` to get the type for a fragment. 120 | 121 | ```tsx 122 | import { gql, OperationData, FragmentData } from "@ts-gql/tag"; 123 | 124 | let myQuery = gql` 125 | query MyQuery { 126 | hello 127 | } 128 | ` as import("../__generated__/ts-gql/MyQuery.ts").type; 129 | 130 | type MyQueryType = OperationData; 131 | 132 | let myFragment = gql` 133 | query MyFragment_something on SomeType { 134 | hello 135 | } 136 | ` as import("../__generated__/ts-gql/MyFragment_something.ts").type; 137 | 138 | type MyFragmentType = FragmentData; 139 | ``` 140 | 141 | ### This seems a lot like Relay, why not just use Relay? 142 | 143 | You're right! There are a lot of similarities between Relay and ts-gql. There are some important differences though. Relay is an entire GraphQL client, it can do a lot of cool things because of that but that also means that if you want the things that the Relay compiler offers, you have to use Relay which may not appeal to everyone. If Relay does work well for you though, that's fine too, use it! 144 | 145 | ts-gql isn't trying to be a GraphQL client, it's only trying to provide a way to type GraphQL queries so that clients can . 146 | 147 | ## Non-Goals 148 | 149 | - Improve the experience of creating GraphQL APIs 150 | 151 | ## Thanks 152 | 153 | - [graphql-code-generator](https://github.com/dotansimha/graphql-code-generator) for the infrastructure to generate TypeScript types from GraphQL queries 154 | - [graphql-let](https://github.com/piglovesyou/graphql-let) for providing a really nice experience 155 | - [Relay](https://github.com/facebook/relay) for their approach with fragments 156 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "12" } }], 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": ["@babel/plugin-transform-runtime", "babel-plugin-macros"] 8 | } 9 | -------------------------------------------------------------------------------- /docs/no-transform.md: -------------------------------------------------------------------------------- 1 | # Using ts-gql without the Babel plugin 2 | 3 | If you want to use ts-gql without the Babel plugin, it is largely the same as using ts-gql with the Babel plugin except that when you use a fragment, you need to interpolate the fragment as well. Note that the same constraints as using ts-gql normally still apply like fragments/operations having unique names. 4 | 5 | First, you need to set the `mode` in your `ts-gql` config to `"no-transform"`. 6 | 7 | ```json 8 | "ts-gql": { 9 | "schema": "schema.graphql", 10 | "mode": "no-transform" 11 | } 12 | ``` 13 | 14 | You can also set the mode to `"mixed"` to [migrate incrementally](#migrating). 15 | 16 | A query with no fragments works the same as when using the Babel plugin except that you import from `@ts-gql/tag/no-transform` rather than `@ts-gql/tag`. You write the query as you did before. 17 | 18 | ```tsx 19 | import { gql } from "@ts-gql/tag/no-transform"; 20 | 21 | gql` 22 | query PostListPage { 23 | posts { 24 | title 25 | author { 26 | name 27 | } 28 | } 29 | } 30 | `; 31 | ``` 32 | 33 | And the ESLint plugin will add the cast as 34 | 35 | ```tsx 36 | import { gql } from "@ts-gql/tag/no-transform"; 37 | 38 | const query = gql` 39 | query PostListPage { 40 | posts { 41 | title 42 | author { 43 | name 44 | } 45 | } 46 | } 47 | ` as import("../../__generated__/ts-gql/PostListPage").type; 48 | ``` 49 | 50 | When you want to write a fragment that doesn't itself use fragments, that works the same as before but with the import changed. 51 | 52 | ```tsx 53 | import { gql } from "@ts-gql/tag/no-transform"; 54 | 55 | const fragment = gql` 56 | fragment PostList_posts on Post { 57 | title 58 | author { 59 | name 60 | } 61 | } 62 | ` as import("../../__generated__/ts-gql/PostList_posts").type; 63 | ``` 64 | 65 | The difference comes in when you want to use a fragment. In the new mode, when you use a fragment like this 66 | 67 | ```tsx 68 | import { gql } from "@ts-gql/tag/no-transform"; 69 | 70 | const query = gql` 71 | query PostListPage { 72 | posts { 73 | ...PostList_posts 74 | } 75 | } 76 | ` as import("../../__generated__/ts-gql/PostListPage").type; 77 | ``` 78 | 79 | If you have ESLint and TypeScript setup in your editor, you'll probably see something like this: 80 | 81 | ``` 82 | When using @ts-gql/tag/no-runtime, all of the fragments that are used must be interpolated, 1 fragment is used but 0 fragments are interpolated eslint(@ts-gql/ts-gql) 83 | 84 | Conversion of type 'TypedDocumentNodeToBeCast' to type 'TypedDocumentNode<{ type: "query"; result: PostListPageQuery; variables: Exact<{ [key: string]: never; }>; documents: TSGQLDocuments; fragments: TSGQLRequiredFragments<{ PostList_posts: true; }>; }>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. 85 | The types of '___type.fragments' are incompatible between these types. 86 | Type 'ProvidedFragments<"none">' is not comparable to type 'TSGQLRequiredFragments<{ PostList_posts: true; }>'. 87 | Types of parameters 'requiredFragments' and 'providedFragments' are incompatible. 88 | Type '{ PostList_posts: true; }' is not comparable to type '"none"'. ts(2352) 89 | ``` 90 | 91 | The ESLint error tells you what to do here, you need to interpolate the fragment in the `gql` call. 92 | 93 | ```tsx 94 | import { gql } from "@ts-gql/tag/no-transform"; 95 | 96 | const fragment = gql` 97 | fragment PostList_posts on Post { 98 | title 99 | author { 100 | name 101 | } 102 | } 103 | ` as import("../../__generated__/ts-gql/PostList_posts").type; 104 | 105 | const query = gql` 106 | query PostListPage { 107 | posts { 108 | ...PostList_posts 109 | } 110 | } 111 | ${fragment} 112 | ` as import("../../__generated__/ts-gql/PostListPage").type; 113 | ``` 114 | 115 | You can pretty much ignore that TypeScript error in general and just look at the ESLint rule error to see what to do. If you interpolate the _wrong fragment_ but the _right number of fragments_ though then you'll only get that TypeScript error because the ESLint rule doesn't know what you've actually interpolated but TypeScript does. 116 | 117 |
Doesn't typescript-eslint allow ESLint rules to use type information? 118 | 119 | Yes! You're correct, it does. It comes with a cost though, it slows down linting performance. Given that interpolating the wrong fragment should be reasonably rare and once you know what to look for, understanding the error is reasonable, ts-gql has opted not to depend on type information in the ESLint rule. 120 | 121 |
122 | 123 | For example, let's say we had this. 124 | 125 | ```tsx 126 | import { gql } from "@ts-gql/tag/no-transform"; 127 | 128 | const postListFragment = gql` 129 | fragment PostList_posts on Post { 130 | title 131 | author { 132 | name 133 | } 134 | } 135 | ` as import("../../__generated__/ts-gql/PostList_posts").type; 136 | 137 | const authorListFragment = gql` 138 | fragment AuthorList_author on Author { 139 | name 140 | } 141 | ` as import("../../__generated__/ts-gql/AuthorList_author").type; 142 | 143 | const query = gql` 144 | query PostListPage { 145 | posts { 146 | ...PostList_posts 147 | } 148 | } 149 | ${authorListFragment} 150 | ` as import("../../__generated__/ts-gql/PostListPage").type; 151 | ``` 152 | 153 | We'll get an error that looks like this: 154 | 155 | ``` 156 | Conversion of type 'TypedDocumentNodeToBeCast<"AuthorList_author">' to type 'TypedDocumentNode<{ type: "query"; result: PostListPageQuery; variables: Exact<{ [key: string]: never; }>; documents: TSGQLDocuments; fragments: TSGQLRequiredFragments<{ PostList_posts: true; }>; }>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. 157 | The types of '___type.fragments' are incompatible between these types. 158 | Type 'ProvidedFragments<{ AuthorList_author: true; }>' is not comparable to type 'TSGQLRequiredFragments<{ PostList_posts: true; }>'. 159 | Types of parameters 'requiredFragments' and 'providedFragments' are incompatible. 160 | Property 'AuthorList_author' is missing in type '{ PostList_posts: true; }' but required in type '{ AuthorList_author: true; }'. ts(2352) 161 | ``` 162 | 163 | Most of this isn't interesting and it's important to note that `If this was intentional, convert the expression to 'unknown' first.` doesn't apply here, you shouldn't cast the call to `unknown`. The part that you should focus on is at the bottom: 164 | 165 | ``` 166 | Types of parameters 'requiredFragments' and 'providedFragments' are incompatible. 167 | Property 'AuthorList_author' is missing in type '{ PostList_posts: true; }' but required in type '{ AuthorList_author: true; }'. ts(2352) 168 | ``` 169 | 170 | This is indicating that we need to interpolate the fragment with the name `AuthorList_author` but we're currently interpolating a fragment with the name `PostList_posts`. So when we interpolate the right fragment, the error will go away. 171 | 172 | ```tsx 173 | import { gql } from "@ts-gql/tag/no-transform"; 174 | 175 | const postListFragment = gql` 176 | fragment PostList_posts on Post { 177 | title 178 | author { 179 | name 180 | } 181 | } 182 | ` as import("../../__generated__/ts-gql/PostList_posts").type; 183 | 184 | const authorListFragment = gql` 185 | fragment AuthorList_author on Author { 186 | name 187 | } 188 | ` as import("../../__generated__/ts-gql/AuthorList_author").type; 189 | 190 | const query = gql` 191 | query PostListPage { 192 | posts { 193 | ...PostList_posts 194 | } 195 | } 196 | ${postListFragment} 197 | ` as import("../../__generated__/ts-gql/PostListPage").type; 198 | ``` 199 | 200 | ## Migrating 201 | 202 | To ease with migrating, you can set the mode to `"mixed"` instead of `"no-transform"`, this will allow importing from both `@ts-gql/tag` and `@ts-gql/tag/no-transform`. Note when using `"mixed"`, you still need to use the Babel plugin. When you're done migrating, you can change the mode to`"no-transform"` and you won't need to use the Babel plugin. 203 | 204 | ```json 205 | "ts-gql": { 206 | "schema": "schema.graphql", 207 | "mode": "mixed" 208 | } 209 | ``` 210 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/repo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "private": true, 6 | "repository": "https://github.com/Thinkmill/ts-gql", 7 | "license": "MIT", 8 | "packageManager": "pnpm@9.15.5", 9 | "engines": { 10 | "pnpm": "9.15.5" 11 | }, 12 | "dependencies": { 13 | "@babel/core": "^7.19.3", 14 | "@babel/plugin-transform-runtime": "^7.19.1", 15 | "@babel/preset-env": "^7.19.4", 16 | "@babel/preset-react": "^7.18.6", 17 | "@babel/preset-typescript": "^7.18.6", 18 | "@changesets/changelog-github": "^0.4.7", 19 | "@changesets/cli": "^2.25.0", 20 | "@manypkg/cli": "0.19.2", 21 | "@preconstruct/cli": "^2.8.3", 22 | "@ts-gql/eslint-plugin": "*", 23 | "@types/jest": "^29.1.2", 24 | "@types/node": "^22.13.1", 25 | "@typescript-eslint/eslint-plugin": "^5.40.0", 26 | "@typescript-eslint/parser": "^6.3.0", 27 | "babel-jest": "^29.1.2", 28 | "babel-plugin-macros": "^3.1.0", 29 | "eslint": "^8.25.0", 30 | "graphql": "^16.10.0", 31 | "jest": "^29.1.2", 32 | "prettier": "^2.7.1", 33 | "typescript": "^5.1.6" 34 | }, 35 | "manypkg": { 36 | "defaultBranch": "main" 37 | }, 38 | "scripts": { 39 | "postinstall": "preconstruct dev && manypkg check && cd test-app && pnpm run build", 40 | "start": "cd test-app && pnpx next", 41 | "release": "preconstruct build && changeset publish", 42 | "version": "changeset version && pnpm i --frozen-lockfile=false && pnpm run format", 43 | "test": "jest", 44 | "types": "tsc", 45 | "lint": "eslint . --ext .ts,.tsx", 46 | "format": "prettier --write .", 47 | "format:check": "prettier --check ." 48 | }, 49 | "preconstruct": { 50 | "packages": [ 51 | "packages/*" 52 | ], 53 | "distFilenameStrategy": "unscoped-package-name", 54 | "exports": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/apollo/README.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/apollo 2 | 3 | This package provides more specific TypeScript types for use with `@ts-gql/apollo`, it can still be used but likely will not be improved. The recommended approach is now to use the `@apollo/client` package directly but this package still exists for easy migration. 4 | 5 | [See the changelog for more details](CHANGELOG.md). 6 | -------------------------------------------------------------------------------- /packages/apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/apollo", 3 | "version": "0.12.2", 4 | "main": "dist/apollo.cjs.js", 5 | "module": "dist/apollo.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/apollo.esm.js", 9 | "default": "./dist/apollo.cjs.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@ts-gql/tag": "^0.7.2" 16 | }, 17 | "peerDependencies": { 18 | "@apollo/client": "*", 19 | "graphql": "*", 20 | "react": "^16.8" 21 | }, 22 | "devDependencies": { 23 | "@apollo/client": "^3.6.9", 24 | "graphql": "^16.10.0", 25 | "react": "^16.14.0" 26 | }, 27 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/apollo" 28 | } 29 | -------------------------------------------------------------------------------- /packages/apollo/src/index.js: -------------------------------------------------------------------------------- 1 | export { useQuery, useMutation, useApolloClient } from "@apollo/client"; 2 | -------------------------------------------------------------------------------- /packages/babel-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/babel-plugin 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - [`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad) Thanks [@emmatown](https://github.com/emmatown)! - Add `exports` field to `package.json` 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - [`e4c60ad`](https://github.com/Thinkmill/ts-gql/commit/e4c60adcc45abba018c4b9d4d0379e7d529a9af1) Thanks [@emmatown](https://github.com/emmatown)! - Initial release 14 | -------------------------------------------------------------------------------- /packages/babel-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/babel-plugin", 3 | "version": "0.1.1", 4 | "main": "dist/babel-plugin.cjs.js", 5 | "module": "dist/babel-plugin.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/babel-plugin.esm.js", 9 | "default": "./dist/babel-plugin.cjs.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "files": [ 14 | "dist", 15 | "!**/*.d.ts" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "@babel/helper-module-imports": "^7.8.3", 20 | "@babel/runtime": "^7.9.2" 21 | }, 22 | "devDependencies": { 23 | "@types/babel__core": "^7.1.9" 24 | }, 25 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/babel-plugin" 26 | } 27 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginObj } from "@babel/core"; 2 | import nodePath from "path"; 3 | // @ts-ignore 4 | import { addNamed } from "@babel/helper-module-imports"; 5 | 6 | export default function plugin(): PluginObj { 7 | return { 8 | visitor: { 9 | TSAsExpression(asExpressionPath) { 10 | let expressionPath = asExpressionPath.get("expression"); 11 | if ( 12 | expressionPath.node.type === "TaggedTemplateExpression" && 13 | expressionPath.node.tag.type === "Identifier" && 14 | expressionPath.node.tag.name === "gql" && 15 | asExpressionPath.node.type === "TSAsExpression" && 16 | asExpressionPath.node.typeAnnotation.type === "TSImportType" 17 | ) { 18 | expressionPath.replaceWith( 19 | addNamed( 20 | expressionPath, 21 | "document", 22 | asExpressionPath.node.typeAnnotation.argument.value, 23 | { 24 | nameHint: nodePath.basename( 25 | asExpressionPath.node.typeAnnotation.argument.value 26 | ), 27 | } 28 | ) 29 | ); 30 | } 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/compiler/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | require("./cli"); 5 | -------------------------------------------------------------------------------- /packages/compiler/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/compiler.cjs.js", 3 | "module": "dist/compiler.esm.js" 4 | } 5 | -------------------------------------------------------------------------------- /packages/compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/compiler", 3 | "version": "0.16.8", 4 | "main": "dist/compiler.cjs.js", 5 | "module": "dist/compiler.esm.js", 6 | "exports": { 7 | "./cli": { 8 | "module": "./cli/dist/compiler.esm.js", 9 | "default": "./cli/dist/compiler.cjs.js" 10 | }, 11 | ".": { 12 | "module": "./dist/compiler.esm.js", 13 | "default": "./dist/compiler.cjs.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "license": "MIT", 18 | "files": [ 19 | "dist", 20 | "cli" 21 | ], 22 | "bin": { 23 | "ts-gql": "./bin.js" 24 | }, 25 | "dependencies": { 26 | "@babel/code-frame": "^7.8.3", 27 | "@babel/parser": "^7.9.6", 28 | "@babel/runtime": "^7.9.2", 29 | "@babel/types": "^7.9.6", 30 | "@nodelib/fs.walk": "^1.2.4", 31 | "@ts-gql/config": "^0.9.2", 32 | "chokidar": "^3.4.0", 33 | "find-pkg-json-field-up": "^1.0.1", 34 | "graceful-fs": "^4.2.4", 35 | "slash": "^3.0.0", 36 | "strip-ansi": "^6.0.0" 37 | }, 38 | "peerDependencies": { 39 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16" 40 | }, 41 | "devDependencies": { 42 | "@graphql-codegen/plugin-helpers": "^5.0.3", 43 | "@ts-gql/tag": "*", 44 | "@types/babel__code-frame": "^7.0.1", 45 | "@types/graceful-fs": "^4.1.3", 46 | "fixturez": "^1.1.0", 47 | "graphql": "^16.10.0", 48 | "lazy-require.macro": "^0.1.0", 49 | "tempy": "1.0.1" 50 | }, 51 | "preconstruct": { 52 | "entrypoints": [ 53 | "index.ts", 54 | "cli.ts" 55 | ] 56 | }, 57 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/compiler" 58 | } 59 | -------------------------------------------------------------------------------- /packages/compiler/src/build.ts: -------------------------------------------------------------------------------- 1 | import { getGeneratedTypes } from "./get-generated-types"; 2 | import { getConfig } from "@ts-gql/config"; 3 | import { applyFsOperation } from "./fs-operations"; 4 | 5 | export async function build(cwd: string) { 6 | let { fsOperations, errors } = await getGeneratedTypes( 7 | await getConfig(cwd), 8 | true 9 | ); 10 | await Promise.all( 11 | fsOperations.map(async (operation) => { 12 | await applyFsOperation(operation); 13 | if (operation.type === "output") { 14 | console.log(`updated ${operation.filename}`); 15 | } else { 16 | console.log(`removed ${operation.filename}`); 17 | } 18 | }) 19 | ); 20 | if (errors.length) { 21 | for (let error of errors) { 22 | console.error(error); 23 | } 24 | process.exit(1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/compiler/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { getGeneratedTypes } from "./get-generated-types"; 2 | import { getConfig } from "@ts-gql/config"; 3 | import { watch } from "./watch"; 4 | import { build } from "./build"; 5 | 6 | let arg = process.argv[2]; 7 | 8 | let commands: Record Promise> = { 9 | async check() { 10 | let { fsOperations, errors } = await getGeneratedTypes( 11 | await getConfig(process.cwd()), 12 | true 13 | ); 14 | for (let error of errors) { 15 | console.error(error); 16 | } 17 | 18 | for (let file of fsOperations) { 19 | if (file.type === "output") { 20 | console.error(`${file.filename} is not up to date`); 21 | } else { 22 | console.error( 23 | `${file.filename} should not exist because it corresponds to a GraphQL document that does not exist or is invalid` 24 | ); 25 | } 26 | } 27 | if (fsOperations.length || errors.length) { 28 | process.exit(1); 29 | } 30 | }, 31 | async build() { 32 | build(process.cwd()); 33 | }, 34 | watch: async () => { 35 | watch(process.cwd()); 36 | }, 37 | }; 38 | 39 | let command = commands[arg]; 40 | 41 | if (!command) { 42 | console.error( 43 | `The command ${arg} does not exist, try one of ${Object.keys(commands).join( 44 | ", " 45 | )}` 46 | ); 47 | process.exit(1); 48 | } 49 | 50 | command(); 51 | -------------------------------------------------------------------------------- /packages/compiler/src/extract-documents.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from "@babel/types"; 2 | import { lazyRequire } from "lazy-require.macro"; 3 | import { CompilerError, FullSourceLocation } from "./types"; 4 | 5 | type BabelVisitors = { 6 | [Type in Node["type"]]?: (node: Extract) => void; 7 | }; 8 | 9 | function babelVisit(node: Node, visitors: BabelVisitors) { 10 | const visitor = visitors[node.type]; 11 | if (visitor !== undefined) { 12 | // @ts-ignore 13 | visitor(node); 14 | return; 15 | } 16 | babelTraverse(node, visitors); 17 | } 18 | 19 | function babelTraverse(node: Node, visitors: BabelVisitors) { 20 | for (const key in node) { 21 | // @ts-ignore 22 | const prop = node[key]; 23 | if (prop && typeof prop === "object" && typeof prop.type === "string") { 24 | babelVisit(prop, visitors); 25 | } else if (Array.isArray(prop)) { 26 | prop.forEach((item) => { 27 | if (item && typeof item === "object" && typeof item.type === "string") { 28 | babelVisit(item, visitors); 29 | } 30 | }); 31 | } 32 | } 33 | } 34 | 35 | export function extractGraphQLDocumentsContentsFromFile( 36 | filename: string, 37 | content: string 38 | ) { 39 | let errors: CompilerError[] = []; 40 | let documents: { 41 | loc: FullSourceLocation; 42 | document: string; 43 | }[] = []; 44 | if (/gql\s*`/.test(content)) { 45 | try { 46 | let ast = lazyRequire().parse(content, { 47 | allowImportExportEverywhere: true, 48 | allowReturnOutsideFunction: true, 49 | allowSuperOutsideMethod: true, 50 | sourceType: "module", 51 | plugins: [ 52 | "asyncGenerators", 53 | "classProperties", 54 | ["decorators", { decoratorsBeforeExport: true }], 55 | "doExpressions", 56 | "dynamicImport", 57 | "typescript", 58 | "functionBind", 59 | "functionSent", 60 | "jsx", 61 | "nullishCoalescingOperator", 62 | "objectRestSpread", 63 | "optionalChaining", 64 | "optionalCatchBinding", 65 | ], 66 | strictMode: false, 67 | }); 68 | babelTraverse(ast as any, { 69 | TSAsExpression(node) { 70 | // TODO: verify operation name === import 71 | if ( 72 | node.typeAnnotation.type === "TSImportType" && 73 | node.expression.type === "TaggedTemplateExpression" 74 | ) { 75 | let isGqlTag = 76 | node.expression.tag.type === "Identifier" && 77 | node.expression.tag.name === "gql"; 78 | let hasOnlyWhitespaceAfterFirstInterpolation = 79 | node.expression.quasi.quasis 80 | .slice(1) 81 | .every((x) => x.value.cooked!.trim() === ""); 82 | if (isGqlTag && hasOnlyWhitespaceAfterFirstInterpolation) { 83 | documents.push({ 84 | loc: node.expression.quasi.loc!, 85 | document: node.expression.quasi.quasis[0].value.cooked!, 86 | }); 87 | } 88 | } 89 | }, 90 | }); 91 | } catch (err) { 92 | if (typeof err.loc?.line === "number") { 93 | errors.push({ 94 | filename, 95 | message: err.message.replace( 96 | ` (${err.loc.line}:${err.loc.column})`, 97 | "" 98 | ), 99 | loc: { start: err.loc }, 100 | }); 101 | } else { 102 | errors.push({ filename, message: err.message }); 103 | } 104 | } 105 | } 106 | return { errors, documents }; 107 | } 108 | -------------------------------------------------------------------------------- /packages/compiler/src/fs-operations.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | 3 | export type FsOperation = 4 | | { 5 | type: "output"; 6 | filename: string; 7 | content: string; 8 | } 9 | | { 10 | type: "remove"; 11 | filename: string; 12 | }; 13 | 14 | export async function applyFsOperation(operation: FsOperation) { 15 | if (operation.type === "remove") { 16 | return fs.unlink(operation.filename); 17 | } 18 | return fs.writeFile(operation.filename, operation.content); 19 | } 20 | -------------------------------------------------------------------------------- /packages/compiler/src/fs.ts: -------------------------------------------------------------------------------- 1 | import gracefulFS from "graceful-fs"; 2 | import { promisify } from "util"; 3 | 4 | export const writeFile = promisify(gracefulFS.writeFile); 5 | export const readFile = promisify(gracefulFS.readFile); 6 | export const mkdir = promisify(gracefulFS.mkdir); 7 | export const readdir = promisify(gracefulFS.readdir); 8 | export const unlink = promisify(gracefulFS.unlink); 9 | -------------------------------------------------------------------------------- /packages/compiler/src/get-documents.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | import type { DocumentNode } from "graphql"; 3 | import { GraphQLError } from "graphql/error/GraphQLError"; 4 | import { parse } from "graphql/language/parser"; 5 | import { extractGraphQLDocumentsContentsFromFile } from "./extract-documents"; 6 | import { 7 | CompilerError, 8 | FullSourceLocation, 9 | TSGQLDocument, 10 | NamedOperationDefinitionNode, 11 | NamedFragmentDefinitionNode, 12 | } from "./types"; 13 | import babelParserPkgJson from "@babel/parser/package.json"; 14 | import { integrity, hashString, locFromSourceAndGraphQLError } from "./utils"; 15 | 16 | type DocumentExtractionCache = { 17 | [filename: string]: { 18 | hash: string; 19 | errors: CompilerError[]; 20 | documents: { 21 | loc: FullSourceLocation; 22 | document: string; 23 | }[]; 24 | }; 25 | }; 26 | 27 | let documentExtractionCacheVersion = 28 | "ts-gql-document-extractor@v3," + 29 | "@babel/parser@" + 30 | babelParserPkgJson.version; 31 | async function readDocumentExtractionCache(cacheFilename: string) { 32 | let cacheContents: string; 33 | try { 34 | cacheContents = await fs.readFile(cacheFilename, "utf8"); 35 | } catch (err) { 36 | if (err.code === "ENOENT") { 37 | return {}; 38 | } 39 | throw err; 40 | } 41 | if (!integrity.verify(cacheContents)) { 42 | return {}; 43 | } 44 | let parsed: { 45 | version: string; 46 | integrity: string; 47 | cache: DocumentExtractionCache; 48 | } = JSON.parse(cacheContents); 49 | if (parsed.version !== documentExtractionCacheVersion) { 50 | return {}; 51 | } 52 | return parsed.cache; 53 | } 54 | function writeDocumentExtractionCache( 55 | cacheFilename: string, 56 | cache: DocumentExtractionCache 57 | ) { 58 | let stringified = JSON.stringify( 59 | { 60 | version: documentExtractionCacheVersion, 61 | integrity: integrity.placeholder, 62 | cache, 63 | }, 64 | null, 65 | 2 66 | ); 67 | let signed = integrity.sign(stringified); 68 | return fs.writeFile(cacheFilename, signed); 69 | } 70 | 71 | export async function getDocuments(files: string[], cacheFilename: string) { 72 | let allErrors: CompilerError[] = []; 73 | let allDocuments: TSGQLDocument[] = []; 74 | let cache = await readDocumentExtractionCache(cacheFilename); 75 | let newCache: DocumentExtractionCache = {}; 76 | await Promise.all( 77 | files.map(async (filename) => { 78 | let contents = await fs.readFile(filename, "utf8"); 79 | let hash = hashString(contents); 80 | if (cache[filename]?.hash !== hash) { 81 | cache[filename] = { 82 | hash, 83 | ...extractGraphQLDocumentsContentsFromFile(filename, contents), 84 | }; 85 | } 86 | newCache[filename] = cache[filename]; 87 | const { documents, errors } = cache[filename]; 88 | allErrors.push(...errors); 89 | 90 | documents.forEach((document) => { 91 | try { 92 | let ast = parse(document.document); 93 | allDocuments.push({ 94 | node: getGqlNode(ast), 95 | loc: document.loc, 96 | filename, 97 | }); 98 | } catch (err) { 99 | if (err instanceof GraphQLError) { 100 | allErrors.push({ 101 | filename, 102 | message: err.message, 103 | loc: locFromSourceAndGraphQLError(document.loc, err), 104 | }); 105 | } else { 106 | allErrors.push({ 107 | filename, 108 | message: err.message, 109 | loc: document.loc, 110 | }); 111 | } 112 | } 113 | }); 114 | }) 115 | ); 116 | await writeDocumentExtractionCache(cacheFilename, newCache); 117 | return { errors: allErrors, documents: allDocuments }; 118 | } 119 | 120 | function getGqlNode(ast: DocumentNode) { 121 | if (ast.definitions.length !== 1) { 122 | throw new GraphQLError( 123 | "GraphQL documents must only have a single operation or fragment definition", 124 | [ast.definitions[1]] 125 | ); 126 | } 127 | let [firstNode] = ast.definitions; 128 | 129 | if ( 130 | firstNode.kind !== "FragmentDefinition" && 131 | firstNode.kind !== "OperationDefinition" 132 | ) { 133 | throw new GraphQLError( 134 | "Only fragments and operations are allowed", 135 | firstNode 136 | ); 137 | } 138 | 139 | if (!firstNode.name) { 140 | throw new GraphQLError("Operations must have names", firstNode); 141 | } 142 | return firstNode as 143 | | NamedOperationDefinitionNode 144 | | NamedFragmentDefinitionNode; 145 | } 146 | -------------------------------------------------------------------------------- /packages/compiler/src/index.ts: -------------------------------------------------------------------------------- 1 | export { watch } from "./watch"; 2 | export { build } from "./build"; 3 | -------------------------------------------------------------------------------- /packages/compiler/src/inline-fragments.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | DocumentNode, 3 | FragmentDefinitionNode, 4 | InlineFragmentNode, 5 | GraphQLSchema, 6 | SelectionSetNode, 7 | FieldNode, 8 | } from "graphql"; 9 | import { lazyRequire } from "lazy-require.macro"; 10 | 11 | export function inlineIntoFirstOperationOrFragment( 12 | document: DocumentNode, 13 | schema: GraphQLSchema 14 | ): DocumentNode { 15 | // we don't want to modify the existing document 16 | document = JSON.parse(JSON.stringify(document)) as DocumentNode; 17 | 18 | let firstNode = document.definitions[0]; 19 | if ( 20 | firstNode.kind !== "FragmentDefinition" && 21 | firstNode.kind !== "OperationDefinition" 22 | ) { 23 | throw new Error( 24 | "First node must be a FragmentDefinition or OperationDefinition" 25 | ); 26 | } 27 | let fragmentsByName: Record = {}; 28 | 29 | document.definitions.forEach((x, i) => { 30 | if (i === 0) return; 31 | if (x.kind !== "FragmentDefinition") { 32 | throw new Error("All non-first nodes must be FragmentDefinition nodes"); 33 | } 34 | fragmentsByName[x.name.value] = x; 35 | }); 36 | const { visit } = lazyRequire(); 37 | 38 | visit(document, { 39 | FragmentSpread(node, key, parent) { 40 | let fragment = fragmentsByName[node.name.value]; 41 | if (!Array.isArray(parent)) { 42 | throw new Error("Unexpected fragment"); 43 | } 44 | if (typeof key !== "number") { 45 | throw new Error("unexpected non-number key"); 46 | } 47 | let inlineSpread: InlineFragmentNode = { 48 | kind: "InlineFragment" as InlineFragmentNode["kind"], 49 | selectionSet: fragment.selectionSet, 50 | typeCondition: fragment.typeCondition, 51 | }; 52 | parent[key] = inlineSpread; 53 | }, 54 | }); 55 | let newDocument = { 56 | kind: "Document" as DocumentNode["kind"], 57 | definitions: [firstNode], 58 | } as const; 59 | 60 | removeUnnecessaryFragmentSpreads(newDocument, schema); 61 | 62 | return newDocument; 63 | } 64 | 65 | function findLast( 66 | arr: readonly Item[], 67 | fn: (item: Item) => Return 68 | ): Return | undefined { 69 | let index = arr.length - 1; 70 | while (index) { 71 | let item = fn(arr[index]); 72 | if (item !== undefined) { 73 | return item; 74 | } 75 | index--; 76 | } 77 | } 78 | 79 | // I'm pretty sure this is incomplete, I think this needs more work for interfaces and unions 80 | function removeUnnecessaryFragmentSpreads( 81 | document: DocumentNode, 82 | schema: GraphQLSchema 83 | ) { 84 | const typeInfoExports = 85 | lazyRequire(); 86 | const { TypeInfo } = typeInfoExports; 87 | const visitorExports = 88 | lazyRequire(); 89 | const visitWithTypeInfo: (typeof typeInfoExports)["visitWithTypeInfo"] = 90 | typeInfoExports.visitWithTypeInfo || 91 | (visitorExports as any).visitWithTypeInfo; 92 | 93 | const { GraphQLObjectType } = 94 | lazyRequire(); 95 | 96 | let typeInfo = new TypeInfo(schema); 97 | 98 | visitorExports.visit( 99 | document, 100 | visitWithTypeInfo(typeInfo, { 101 | InlineFragment: { 102 | leave(node, key, parent, path, ancestors) { 103 | if ( 104 | node.typeCondition && 105 | (node.directives === undefined || node.directives.length === 0) 106 | ) { 107 | let parentType = typeInfo.getParentType(); 108 | if (!parentType) { 109 | throw new Error("unexpected no parent type"); 110 | } 111 | if (parentType instanceof GraphQLObjectType) { 112 | let selectionSet = findLast(ancestors, (item) => { 113 | if (item && "kind" in item && item.kind === "SelectionSet") { 114 | return item; 115 | } 116 | }); 117 | if (!selectionSet) { 118 | throw new Error( 119 | "SelectionSet not found where one was expected to be" 120 | ); 121 | } 122 | selectionSet.selections = [ 123 | ...selectionSet.selections.filter((x) => x !== node), 124 | ]; 125 | mergeSelectionSets(selectionSet, node.selectionSet); 126 | } 127 | } 128 | }, 129 | }, 130 | }) 131 | ); 132 | } 133 | 134 | // https://github.com/dotansimha/graphql-code-generator/blob/3dac6d8c96eebdd74cb3bb33ce8e376fbc12d848/packages/plugins/other/visitor-plugin-common/src/utils.ts#L338-L373 135 | function mergeSelectionSets( 136 | selectionSet1: SelectionSetNode, 137 | selectionSet2: SelectionSetNode 138 | ): void { 139 | const newSelections = [...selectionSet1.selections]; 140 | for (const selection2 of selectionSet2.selections) { 141 | if (selection2.kind === "FragmentSpread") { 142 | throw new Error("Unexpected fragment spread"); 143 | } 144 | const match = newSelections.find( 145 | (selection1) => 146 | selection1.kind === "Field" && 147 | selection2.kind === "Field" && 148 | getFieldNodeNameValue(selection1) === getFieldNodeNameValue(selection2) 149 | ); 150 | 151 | if (match) { 152 | if ( 153 | match.kind === "Field" && 154 | match.selectionSet && 155 | selection2.selectionSet 156 | ) { 157 | mergeSelectionSets(match.selectionSet, selection2.selectionSet); 158 | } 159 | continue; 160 | } 161 | newSelections.push(selection2); 162 | } 163 | selectionSet1.selections = newSelections; 164 | } 165 | 166 | export const getFieldNodeNameValue = (node: FieldNode): string => { 167 | return (node.alias || node.name).value; 168 | }; 169 | -------------------------------------------------------------------------------- /packages/compiler/src/integrity.ts: -------------------------------------------------------------------------------- 1 | import { hashString } from "./utils"; 2 | 3 | export function getDoesFileHaveIntegrity(content: string) { 4 | let hash: string | undefined; 5 | content = content.replace( 6 | /^\/\/ ts-gql-integrity:([a-f0-9]{32})\n/, 7 | (match, md5) => { 8 | hash = md5; 9 | return ""; 10 | } 11 | ); 12 | if (!hash) { 13 | return false; 14 | } 15 | return hash === hashString(content); 16 | } 17 | 18 | export function wrapFileInIntegrityComment(content: string) { 19 | return `// ts-gql-integrity:${hashString(content)}\n${content}`; 20 | } 21 | -------------------------------------------------------------------------------- /packages/compiler/src/introspection-result.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | import type { GraphQLSchema } from "graphql"; 3 | import { parseTsGqlMeta } from "./utils"; 4 | import { FsOperation } from "./fs-operations"; 5 | import { 6 | getDoesFileHaveIntegrity, 7 | wrapFileInIntegrityComment, 8 | } from "./integrity"; 9 | import { Config } from "@ts-gql/config"; 10 | import { lazyRequire } from "lazy-require.macro"; 11 | 12 | async function generateIntrospectionResult( 13 | schema: GraphQLSchema, 14 | schemaHash: string, 15 | filename: string 16 | ): Promise { 17 | const { introspectionFromSchema } = 18 | lazyRequire(); 19 | const introspection = introspectionFromSchema(schema, { 20 | descriptions: false, 21 | }); 22 | return { 23 | type: "output", 24 | filename, 25 | content: 26 | wrapFileInIntegrityComment(`/*\nts-gql-meta-begin\n${JSON.stringify( 27 | { 28 | hash: schemaHash, 29 | }, 30 | null, 31 | 2 32 | )}\nts-gql-meta-end\n*/\n 33 | export const result = JSON.parse(${JSON.stringify( 34 | JSON.stringify(introspection) 35 | )}) 36 | `), 37 | }; 38 | } 39 | 40 | export async function cachedGenerateIntrospectionResult( 41 | config: Config, 42 | filename: string 43 | ) { 44 | let schemaHash = config.schemaHash + "v1"; 45 | let types: string; 46 | try { 47 | types = await fs.readFile(filename, "utf8"); 48 | } catch (err) { 49 | if (err.code === "ENOENT") { 50 | return generateIntrospectionResult(config.schema(), schemaHash, filename); 51 | } 52 | throw err; 53 | } 54 | if ( 55 | !getDoesFileHaveIntegrity(types) || 56 | parseTsGqlMeta(types).hash !== schemaHash 57 | ) { 58 | return generateIntrospectionResult(config.schema(), schemaHash, filename); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/compiler/src/operation-types.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | import { DocumentNode, ExecutableDefinitionNode } from "graphql"; 3 | import { codegen } from "./vendor/codegen-core"; 4 | import { hashString, parseTsGqlMeta } from "./utils"; 5 | import { FsOperation } from "./fs-operations"; 6 | import { 7 | getDoesFileHaveIntegrity, 8 | wrapFileInIntegrityComment, 9 | } from "./integrity"; 10 | import stripAnsi from "strip-ansi"; 11 | import { Config } from "@ts-gql/config"; 12 | import { lazyRequire } from "lazy-require.macro"; 13 | import { inlineIntoFirstOperationOrFragment } from "./inline-fragments"; 14 | import { typescriptOperationsPlugin } from "./vendor/typescript-operations"; 15 | 16 | function getUsedFragments(node: ExecutableDefinitionNode) { 17 | const visit = lazyRequire().visit; 18 | const usedFragments = new Set(); 19 | visit(node, { 20 | FragmentSpread(node) { 21 | usedFragments.add(node.name.value); 22 | }, 23 | }); 24 | return [...usedFragments]; 25 | } 26 | 27 | let plugin: 28 | | typeof import("./vendor/typescript-operations").typescriptOperationsPlugin 29 | | null = null; 30 | 31 | async function generateOperationTypes( 32 | config: Config, 33 | operation: DocumentNode, 34 | filename: string, 35 | operationHash: string 36 | ): Promise { 37 | if (!plugin) { 38 | plugin = (await import("./vendor/typescript-operations")) 39 | .typescriptOperationsPlugin; 40 | } 41 | let result = codegen({ 42 | documents: [ 43 | { 44 | document: inlineIntoFirstOperationOrFragment( 45 | operation, 46 | config.schema() 47 | ), 48 | }, 49 | ], 50 | schemaAst: config.schema(), 51 | config: {}, 52 | filename: "", 53 | 54 | plugins: [ 55 | { 56 | "typescript-operations": { 57 | namespacedImportName: "SchemaTypes", 58 | immutableTypes: config.readonlyTypes, 59 | noExport: true, 60 | avoidOptionals: { 61 | field: true, 62 | inputValue: false, 63 | object: false, 64 | defaultValue: false, 65 | }, 66 | nonOptionalTypename: config.addTypename, 67 | skipTypename: !config.addTypename, 68 | namingConvention: "keep", 69 | scalars: config.scalars, 70 | }, 71 | }, 72 | ], 73 | pluginMap: { 74 | "typescript-operations": { plugin }, 75 | }, 76 | }); 77 | 78 | const operationNode = operation.definitions[0]; 79 | if ( 80 | !operationNode || 81 | (operationNode.kind !== "FragmentDefinition" && 82 | operationNode.kind !== "OperationDefinition") 83 | ) { 84 | throw new Error( 85 | "First node in document does not exist or is not a fragment or operation" 86 | ); 87 | } 88 | if (!operationNode.name) { 89 | throw new Error("name not found on OperationDefinition"); 90 | } 91 | const operationType = 92 | operationNode.kind === "OperationDefinition" 93 | ? operationNode.operation 94 | : "fragment"; 95 | 96 | let upperCaseOperationType = 97 | operationType.charAt(0).toUpperCase() + operationType.slice(1); 98 | const usedFragments = getUsedFragments(operationNode); 99 | return { 100 | type: "output", 101 | filename, 102 | content: 103 | wrapFileInIntegrityComment(`/*\nts-gql-meta-begin\n${JSON.stringify( 104 | { hash: operationHash }, 105 | null, 106 | 2 107 | )}\nts-gql-meta-end\n*/\n\nimport * as SchemaTypes from "./@schema";\nimport { TypedDocumentNode } from "@ts-gql/tag";\n\n${result} 108 | 109 | 110 | export type type = TypedDocumentNode<{ 111 | type: ${JSON.stringify(operationType)}; 112 | result: ${operationNode.name.value + upperCaseOperationType};${ 113 | operationType === "fragment" 114 | ? `\n name: ${JSON.stringify(operationNode.name.value)};` 115 | : `\n variables: ${ 116 | operationNode.variableDefinitions?.length 117 | ? operationNode.name.value + 118 | upperCaseOperationType + 119 | "Variables" 120 | : `{}` 121 | };` 122 | } 123 | documents: SchemaTypes.TSGQLDocuments; 124 | fragments: SchemaTypes.TSGQLRequiredFragments<${JSON.stringify( 125 | usedFragments.length === 0 126 | ? "none" 127 | : Object.fromEntries(usedFragments.map((x) => [x, true])) 128 | )}> 129 | }> 130 | 131 | declare module "./@schema" { 132 | interface TSGQLDocuments { 133 | ${operationNode.name.value}: type; 134 | } 135 | } 136 | 137 | export const document = JSON.parse(${JSON.stringify( 138 | JSON.stringify(operation, (key, value) => 139 | key === "loc" ? undefined : value 140 | ) 141 | )}) 142 | `), 143 | }; 144 | } 145 | 146 | function generateErrorModuleFsOperation( 147 | filename: string, 148 | hash: string, 149 | error: string 150 | ) { 151 | return { 152 | type: "output" as const, 153 | filename, 154 | content: 155 | wrapFileInIntegrityComment(`/*\nts-gql-meta-begin\n${JSON.stringify( 156 | { hash }, 157 | null, 158 | 2 159 | )}\nts-gql-meta-end\n*/ 160 | 161 | export type type = never; 162 | 163 | throw new Error(typeof window === 'undefined' ? ${JSON.stringify( 164 | stripAnsi(error) 165 | )} : ${JSON.stringify(error)}); 166 | `), 167 | }; 168 | } 169 | 170 | export async function cachedGenerateErrorModuleFsOperation( 171 | filename: string, 172 | error: string 173 | ) { 174 | let hash = hashString(error + "v1"); 175 | let types: string; 176 | try { 177 | types = await fs.readFile(filename, "utf8"); 178 | } catch (err) { 179 | if (err.code === "ENOENT") { 180 | return generateErrorModuleFsOperation(filename, hash, error); 181 | } 182 | throw err; 183 | } 184 | if (!getDoesFileHaveIntegrity(types) || parseTsGqlMeta(types).hash !== hash) { 185 | return generateErrorModuleFsOperation(filename, hash, error); 186 | } 187 | } 188 | 189 | export async function cachedGenerateOperationTypes( 190 | config: Config, 191 | operation: DocumentNode, 192 | filename: string, 193 | operationHash: string 194 | ) { 195 | let types: string; 196 | try { 197 | types = await fs.readFile(filename, "utf8"); 198 | } catch (err) { 199 | if (err.code === "ENOENT") { 200 | return generateOperationTypes(config, operation, filename, operationHash); 201 | } 202 | throw err; 203 | } 204 | if ( 205 | !getDoesFileHaveIntegrity(types) || 206 | parseTsGqlMeta(types).hash !== operationHash 207 | ) { 208 | return generateOperationTypes(config, operation, filename, operationHash); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /packages/compiler/src/schema-types.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | import path from "path"; 3 | import { hashString, parseTsGqlMeta } from "./utils"; 4 | import { FsOperation } from "./fs-operations"; 5 | import { 6 | wrapFileInIntegrityComment, 7 | getDoesFileHaveIntegrity, 8 | } from "./integrity"; 9 | import { Config } from "@ts-gql/config"; 10 | import { GraphQLError } from "graphql/error/GraphQLError"; 11 | import { CompilerError } from "./types"; 12 | import { BatchGraphQLError } from "@ts-gql/config"; 13 | import { printSchemaTypes } from "./schema"; 14 | 15 | export class ThrowableCompilerErrorSet extends Error { 16 | compilerErrors: CompilerError[]; 17 | constructor(compilerErrors: CompilerError[]) { 18 | super( 19 | "A ts-gql compiler error occurred. If you're seeing this, there's likely a bug in ts-gql's error printing" 20 | ); 21 | this.compilerErrors = compilerErrors; 22 | } 23 | } 24 | 25 | function generateSchemaTypes( 26 | config: Config, 27 | filename: string, 28 | schemaHash: string 29 | ): FsOperation { 30 | try { 31 | config.schema(); 32 | } catch (err) { 33 | if (err instanceof BatchGraphQLError) { 34 | throw new ThrowableCompilerErrorSet( 35 | err.errors.map((err) => ({ 36 | filename: config.schemaFilename, 37 | message: err.message, 38 | loc: err.locations?.[0] ? { start: err.locations[0] } : undefined, 39 | })) 40 | ); 41 | } 42 | if (err instanceof GraphQLError) { 43 | throw new ThrowableCompilerErrorSet([ 44 | { 45 | filename: config.schemaFilename, 46 | message: err.message, 47 | loc: err.locations?.[0] ? { start: err.locations[0] } : undefined, 48 | }, 49 | ]); 50 | } 51 | throw new ThrowableCompilerErrorSet([ 52 | { 53 | filename: config.schemaFilename, 54 | message: err.toString(), 55 | }, 56 | ]); 57 | } 58 | 59 | return { 60 | type: "output", 61 | filename, 62 | content: wrapFileInIntegrityComment( 63 | `/*\nts-gql-meta-begin\n${JSON.stringify( 64 | { hash: schemaHash }, 65 | null, 66 | 2 67 | )}\nts-gql-meta-end\n*/\n${printSchemaTypes({ 68 | schema: config.schema(), 69 | readonly: config.readonlyTypes, 70 | scalars: config.scalars, 71 | })}\nexport interface TSGQLDocuments extends Record> {}\n\nexport type TSGQLRequiredFragments = (providedFragments: T) => T;` 72 | ), 73 | }; 74 | } 75 | 76 | export async function cachedGenerateSchemaTypes(config: Config) { 77 | let schemaHash = hashString( 78 | config.schemaHash + 79 | JSON.stringify(config.scalars) + 80 | config.readonlyTypes + 81 | "v4" 82 | ); 83 | let types: string; 84 | let filename = path.join( 85 | config.directory, 86 | "__generated__", 87 | "ts-gql", 88 | "@schema.d.ts" 89 | ); 90 | try { 91 | types = await fs.readFile(filename, "utf8"); 92 | } catch (err) { 93 | if (err.code === "ENOENT") { 94 | return generateSchemaTypes(config, filename, schemaHash); 95 | } 96 | throw err; 97 | } 98 | if ( 99 | !getDoesFileHaveIntegrity(types) || 100 | parseTsGqlMeta(types).hash !== schemaHash 101 | ) { 102 | return generateSchemaTypes(config, filename, schemaHash); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/compiler/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLEnumType, 3 | GraphQLField, 4 | GraphQLInputField, 5 | GraphQLInputObjectType, 6 | GraphQLInterfaceType, 7 | GraphQLList, 8 | GraphQLNamedType, 9 | GraphQLNonNull, 10 | GraphQLNullableType, 11 | GraphQLObjectType, 12 | GraphQLScalarType, 13 | GraphQLType, 14 | GraphQLUnionType, 15 | } from "graphql/type/definition"; 16 | import { specifiedScalarTypes } from "graphql/type/scalars"; 17 | import { introspectionTypes } from "graphql/type/introspection"; 18 | 19 | import { GraphQLSchema } from "graphql/type/schema"; 20 | 21 | export type PrinterOptions = { 22 | readonly: boolean; 23 | }; 24 | 25 | const introspectionTypeNames = new Set( 26 | introspectionTypes.map((type) => type.name) 27 | ); 28 | 29 | const builtinScalars: Record = { 30 | ID: "string", 31 | String: "string", 32 | Boolean: "boolean", 33 | Int: "number", 34 | Float: "number", 35 | }; 36 | 37 | export type SchemaPrinterOptions = { 38 | schema: GraphQLSchema; 39 | readonly: boolean; 40 | scalars: Record; 41 | }; 42 | 43 | const deprecatedComment = `/** @deprecated This should not be used outside of code generated by ts-gql */`; 44 | 45 | // the helper types are from GraphQL Code Generator so that when it 46 | // generates the operation types, it has them available 47 | const header = `${deprecatedComment} 48 | export type Maybe = T | null; 49 | ${deprecatedComment} 50 | export type InputMaybe = Maybe; 51 | ${deprecatedComment} 52 | export type Exact = { [K in keyof T]: T[K] }; 53 | ${deprecatedComment} 54 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 55 | ${deprecatedComment} 56 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 57 | 58 | ${deprecatedComment} 59 | export type Scalars = { 60 | `; 61 | 62 | type InternalPrinterOptions = { 63 | inlinables: Map; 64 | readonly: boolean; 65 | }; 66 | 67 | export function printSchemaTypes(options: SchemaPrinterOptions) { 68 | const { schema } = options; 69 | let output = ""; 70 | const allScalars: Record = { 71 | ...builtinScalars, 72 | ...options.scalars, 73 | }; 74 | const inlinables = new Map(); 75 | for (const builtin of specifiedScalarTypes) { 76 | if (options.scalars[builtin.name] === undefined) { 77 | inlinables.set(builtin.name, builtinScalars[builtin.name]); 78 | } 79 | } 80 | const internalOptions: InternalPrinterOptions = { 81 | inlinables, 82 | readonly: options.readonly, 83 | }; 84 | 85 | const typeMap = schema.getTypeMap(); 86 | const allTypes: Set = new Set(specifiedScalarTypes); 87 | const types = Object.values(typeMap); 88 | for (const type of types) { 89 | allTypes.add(type); 90 | } 91 | let scalarsOutput = ""; 92 | for (const type of allTypes) { 93 | if (introspectionTypeNames.has(type.name)) continue; 94 | if (type instanceof GraphQLEnumType) { 95 | output += "\n\n" + printEnumType(type); 96 | } 97 | if (type instanceof GraphQLInputObjectType) { 98 | output += "\n\n" + printInputObjectType(type, internalOptions); 99 | } 100 | if (type instanceof GraphQLObjectType) { 101 | output += "\n\n" + printObjectType(type, internalOptions); 102 | } 103 | if (type instanceof GraphQLInterfaceType) { 104 | output += "\n\n" + printInterfaceType(type, internalOptions); 105 | } 106 | if ( 107 | type instanceof GraphQLObjectType || 108 | type instanceof GraphQLInterfaceType 109 | ) { 110 | for (const field of Object.values(type.getFields())) { 111 | if (field.args.length === 0) continue; 112 | output += 113 | "\n\n" + 114 | `export type ${type.name}${field.name}Args = {\n${printInputFields( 115 | field.args, 116 | internalOptions 117 | )}\n};`; 118 | } 119 | } 120 | if (type instanceof GraphQLUnionType) { 121 | output += 122 | "\n\n" + 123 | `export type ${type.name} = ${type 124 | .getTypes() 125 | .map((type) => type.name) 126 | .join(" | ")};`; 127 | } 128 | 129 | if (type instanceof GraphQLScalarType) { 130 | const inlinedContent = inlinables.get(type.name); 131 | if (inlinedContent !== undefined) { 132 | scalarsOutput += ` ${type.name}: ${inlinedContent};\n`; 133 | continue; 134 | } 135 | output += 136 | "\n\n" + 137 | `export type ${type.name} = ${allScalars[type.name] ?? "any"};`; 138 | 139 | scalarsOutput += ` ${type.name}: ${type.name};\n`; 140 | continue; 141 | } 142 | } 143 | return ( 144 | header + 145 | scalarsOutput + 146 | "};" + 147 | output + 148 | `\n\ntype TSGQLMaybeArray = ${ 149 | options.readonly ? "ReadonlyArray" : "Array" 150 | } | T\n\nexport {};` 151 | ); 152 | } 153 | 154 | function printInputObjectType( 155 | type: GraphQLInputObjectType, 156 | options: InternalPrinterOptions 157 | ) { 158 | if (type.isOneOf) { 159 | return `export type ${type.name} = {\n${Object.values(type.getFields()) 160 | .map((field) => 161 | printInputFields( 162 | [{ ...field, type: new GraphQLNonNull(field.type) }], 163 | options 164 | ) 165 | ) 166 | .join("\n} | {\n")}\n};`; 167 | } 168 | return `export type ${type.name} = {\n${printInputFields( 169 | Object.values(type.getFields()), 170 | options 171 | )}\n};`; 172 | } 173 | 174 | function printObjectType( 175 | type: GraphQLObjectType, 176 | options: InternalPrinterOptions 177 | ) { 178 | return `export type ${type.name} = {\n ${ 179 | options.readonly ? "readonly " : "" 180 | }__typename: ${JSON.stringify(type.name)};\n${printOutputFields( 181 | Object.values(type.getFields()), 182 | options 183 | )}\n};`; 184 | } 185 | 186 | function printInterfaceType( 187 | type: GraphQLInterfaceType, 188 | options: InternalPrinterOptions 189 | ) { 190 | return `export type ${type.name} = {\n${printOutputFields( 191 | Object.values(type.getFields()), 192 | options 193 | )}\n};`; 194 | } 195 | 196 | function printOutputFields( 197 | fields: GraphQLField[], 198 | options: InternalPrinterOptions 199 | ) { 200 | const readonlyPart = options.readonly ? "readonly " : ""; 201 | return fields 202 | .map( 203 | (field) => 204 | ` ${readonlyPart}${field.name}: ${printTypeReference( 205 | field.type, 206 | options, 207 | "output" 208 | )};` 209 | ) 210 | .join("\n"); 211 | } 212 | 213 | function printInputFields( 214 | fields: readonly GraphQLInputField[], 215 | options: InternalPrinterOptions 216 | ) { 217 | const readonlyPart = options.readonly ? "readonly " : ""; 218 | return fields 219 | .map( 220 | (field) => 221 | ` ${readonlyPart}${field.name}${ 222 | field.type instanceof GraphQLNonNull && 223 | field.defaultValue === undefined 224 | ? "" 225 | : "?" 226 | }: ${printTypeReference(field.type, options, "input")};` 227 | ) 228 | .join("\n"); 229 | } 230 | 231 | function printEnumType(type: GraphQLEnumType) { 232 | return `export type ${type.name} =\n${type 233 | .getValues() 234 | .map((value) => ` | "${value.name}"`) 235 | .join("\n")};`; 236 | } 237 | 238 | function printTypeReferenceWithoutNullability( 239 | type: GraphQLNullableType, 240 | options: InternalPrinterOptions, 241 | mode: "input" | "output" 242 | ): string { 243 | if (type instanceof GraphQLList) { 244 | return `${ 245 | mode === "input" 246 | ? "TSGQLMaybeArray" 247 | : options.readonly 248 | ? "ReadonlyArray" 249 | : "Array" 250 | }<${printTypeReference(type.ofType, options, mode)}>`; 251 | } 252 | const inline = options.inlinables.get(type.name); 253 | if (inline !== undefined) { 254 | return inline; 255 | } 256 | return type.name; 257 | } 258 | 259 | function printTypeReference( 260 | type: GraphQLType, 261 | options: InternalPrinterOptions, 262 | mode: "input" | "output" 263 | ): string { 264 | if (type instanceof GraphQLNonNull) { 265 | return printTypeReferenceWithoutNullability(type.ofType, options, mode); 266 | } 267 | const inner = printTypeReferenceWithoutNullability(type, options, mode); 268 | return `${inner} | null`; 269 | } 270 | -------------------------------------------------------------------------------- /packages/compiler/src/test/inline-fragments.test.tsx: -------------------------------------------------------------------------------- 1 | import { buildSchema, validate, parse, print } from "graphql"; 2 | import { inlineIntoFirstOperationOrFragment } from "../inline-fragments"; 3 | import { schema as _schema } from "./test-schema"; 4 | 5 | let schema = buildSchema(_schema); 6 | 7 | const gql = ([str]: TemplateStringsArray) => str; 8 | 9 | function inline(document: string) { 10 | let ast = parse(document); 11 | let errors = validate(schema, ast); 12 | if (errors.length) { 13 | throw errors[0]; 14 | } 15 | return print(inlineIntoFirstOperationOrFragment(ast, schema)); 16 | } 17 | 18 | test("basic", () => { 19 | expect( 20 | inline(gql` 21 | query Thing { 22 | someObj { 23 | arr { 24 | ...Frag_a 25 | } 26 | ...Frag_b 27 | } 28 | } 29 | fragment Frag_a on OutputThing { 30 | other 31 | } 32 | fragment Frag_b on OutputThing { 33 | arr { 34 | id 35 | } 36 | } 37 | `) 38 | ).toMatchInlineSnapshot(` 39 | "query Thing { 40 | someObj { 41 | arr { 42 | other 43 | id 44 | } 45 | } 46 | }" 47 | `); 48 | }); 49 | 50 | test("single spread", () => { 51 | expect( 52 | inline(gql` 53 | query Thing { 54 | someObj { 55 | ...Frag_a 56 | } 57 | } 58 | fragment Frag_a on OutputThing { 59 | other 60 | } 61 | `) 62 | ).toMatchInlineSnapshot(` 63 | "query Thing { 64 | someObj { 65 | other 66 | } 67 | }" 68 | `); 69 | }); 70 | 71 | test("interface", () => { 72 | expect( 73 | inline(gql` 74 | query Thing { 75 | node { 76 | ...Frag_a 77 | ...Frag_b 78 | ...Frag_c 79 | } 80 | } 81 | fragment Frag_a on AnotherNode { 82 | another 83 | } 84 | fragment Frag_b on SomethingNode { 85 | something 86 | } 87 | 88 | fragment Frag_c on Node { 89 | id 90 | } 91 | `) 92 | ).toMatchInlineSnapshot(` 93 | "query Thing { 94 | node { 95 | ... on AnotherNode { 96 | another 97 | } 98 | ... on SomethingNode { 99 | something 100 | } 101 | ... on Node { 102 | id 103 | } 104 | } 105 | }" 106 | `); 107 | }); 108 | -------------------------------------------------------------------------------- /packages/compiler/src/test/test-schema.ts: -------------------------------------------------------------------------------- 1 | let gql = ([str]: TemplateStringsArray) => str; 2 | 3 | export let schema = gql` 4 | type Query { 5 | hello: String! 6 | other: Boolean! 7 | another: String! 8 | something: String 9 | someObj: OutputThing! 10 | arr: [OutputThing!]! 11 | node: Node! 12 | optional(thing: String): String! 13 | oneMore(thing: String, other: Something!): String! 14 | union: [Union]! 15 | json(json: JSON): JSON 16 | enum(a: SomeEnum): SomeEnum 17 | inputObject(a: SomeOneOf, b: OneOfWithOnlyOne): String 18 | } 19 | 20 | input SomeOneOf @oneOf { 21 | a: String 22 | b: Int 23 | c: Boolean 24 | } 25 | 26 | input OneOfWithOnlyOne @oneOf { 27 | a: String 28 | } 29 | 30 | enum SomeEnum { 31 | a 32 | b 33 | } 34 | 35 | scalar JSON 36 | 37 | type Mutation { 38 | hello: String! 39 | other: Boolean! 40 | another: String! 41 | something: String 42 | optional(thing: String): String! 43 | oneMore(thing: String, other: Something!): String! 44 | } 45 | 46 | interface Node { 47 | id: ID! 48 | } 49 | 50 | type SomethingNode implements Node { 51 | id: ID! 52 | something: String! 53 | } 54 | 55 | type AnotherNode implements Node { 56 | id: ID! 57 | another: String! 58 | } 59 | 60 | type OutputThing { 61 | id: ID! 62 | other: String! 63 | arr: [OutputThing!]! 64 | } 65 | 66 | input Something { 67 | yes: Boolean 68 | no: String! 69 | } 70 | 71 | union Union = A | B 72 | 73 | type A { 74 | a: String 75 | } 76 | 77 | type B { 78 | b: String 79 | } 80 | `; 81 | -------------------------------------------------------------------------------- /packages/compiler/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NameNode, 3 | OperationDefinitionNode, 4 | FragmentDefinitionNode, 5 | } from "graphql"; 6 | 7 | export type Position = { 8 | line: number; 9 | column: number; 10 | }; 11 | 12 | export type SourceLocation = { 13 | start: Position; 14 | end?: Position; 15 | }; 16 | 17 | export type FullSourceLocation = { 18 | start: Position; 19 | end: Position; 20 | }; 21 | 22 | export type CompilerError = { 23 | filename: string; 24 | message: string; 25 | loc?: SourceLocation; 26 | }; 27 | 28 | export type NamedOperationDefinitionNode = Omit< 29 | OperationDefinitionNode, 30 | "name" 31 | > & { 32 | readonly name: NameNode; 33 | }; 34 | 35 | export type NamedFragmentDefinitionNode = FragmentDefinitionNode; 36 | 37 | export type TSGQLDocument = { 38 | filename: string; 39 | loc: FullSourceLocation; 40 | node: NamedOperationDefinitionNode | NamedFragmentDefinitionNode; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/compiler/src/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { GraphQLError } from "graphql"; 3 | import { FullSourceLocation } from "./types"; 4 | 5 | export function hashString(input: string) { 6 | let md5sum = crypto.createHash("md5"); 7 | md5sum.update(input); 8 | return md5sum.digest("hex"); 9 | } 10 | 11 | export function parseTsGqlMeta(content: string) { 12 | let result = /ts-gql-meta-begin([^]+)ts-gql-meta-end/m.exec(content); 13 | if (result === null) { 14 | throw new Error( 15 | "could not find ts-gql meta in the following contents:\n" + content 16 | ); 17 | } 18 | return JSON.parse(result[1]); 19 | } 20 | 21 | // based on https://github.com/facebook/fbjs/blob/main/packages/signedsource/index.js 22 | 23 | let integrityPlaceholder = "__TS_GQL_INTEGRITY_PLACEHOLDER__"; 24 | 25 | let pattern = /__TS_GQL_INTEGRITY_([a-f0-9]{32})_TS_GQL_INTEGRITY__/; 26 | 27 | export const integrity = { 28 | placeholder: integrityPlaceholder, 29 | sign(content: string) { 30 | let hash = hashString(content); 31 | return content.replace( 32 | integrityPlaceholder, 33 | `__TS_GQL_INTEGRITY_${hash}_TS_GQL_INTEGRITY__` 34 | ); 35 | }, 36 | verify(content: string) { 37 | const match = pattern.exec(content); 38 | if (!match) { 39 | return false; 40 | } 41 | const unsigned = content.replace(pattern, integrityPlaceholder); 42 | return hashString(unsigned) === match[1]; 43 | }, 44 | }; 45 | 46 | export function locFromSourceAndGraphQLError( 47 | loc: FullSourceLocation, 48 | error: GraphQLError 49 | ) { 50 | if (!error.locations || !error.locations.length) { 51 | return; 52 | } 53 | const gqlLocation = error.locations[0]; 54 | 55 | // TODO: look at nodes instead of locations so we can get the start AND end 56 | return { 57 | start: { 58 | line: loc.start.line + gqlLocation.line - 1, 59 | column: 60 | gqlLocation.line === 1 61 | ? loc.start.column + gqlLocation.column + 1 62 | : gqlLocation.column, 63 | }, 64 | // end: { 65 | // line: loc.start.line + gqlLocation.endToken.line - 1, 66 | // column: 67 | // (gqlLocation.endToken.line === 1 68 | // ? loc.end.column + gqlLocation.endToken.column 69 | // : gqlLocation.endToken.column) - 1, 70 | // }, 71 | }; 72 | } 73 | 74 | export function resolvable(): Promise & { 75 | resolve: (value: T) => void; 76 | reject: (reason?: any) => void; 77 | } { 78 | let _resolve: (value: T) => void; 79 | let _reject: (reason: any) => void; 80 | const promise = new Promise((resolve, reject) => { 81 | _resolve = resolve; 82 | _reject = reject; 83 | }); 84 | return Object.assign(promise, { 85 | resolve: _resolve!, 86 | reject: _reject!, 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /packages/compiler/src/validate-documents.ts: -------------------------------------------------------------------------------- 1 | import type { ValidationRule, DocumentNode } from "graphql"; 2 | import { GraphQLError } from "graphql/error/GraphQLError"; 3 | import nodePath from "path"; 4 | import * as fs from "./fs"; 5 | import { Config } from "@ts-gql/config"; 6 | import { lazyRequire } from "lazy-require.macro"; 7 | import { locFromSourceAndGraphQLError, integrity } from "./utils"; 8 | import { CompilerError, FullSourceLocation } from "./types"; 9 | 10 | const SkipNonFirstFragmentsRule: ValidationRule = () => { 11 | return { 12 | FragmentDefinition(node, key) { 13 | if (key !== 0) { 14 | return null; 15 | } 16 | }, 17 | }; 18 | }; 19 | 20 | const FragmentNameValidationRule: ValidationRule = (context) => { 21 | return { 22 | FragmentDefinition(node) { 23 | let message = 24 | "Fragment names must be in the format ComponentName_propName"; 25 | 26 | if (!node.name) { 27 | context.reportError(new GraphQLError(message, [node])); 28 | } else if (!/.+_.+/.test(node.name.value)) { 29 | context.reportError(new GraphQLError(message, [node.name])); 30 | } 31 | }, 32 | }; 33 | }; 34 | 35 | export type DocumentValidationCache = { 36 | [operationHash: string]: CompilerError[]; 37 | }; 38 | 39 | let getDocumentValidationCacheVersion = (config: Config) => 40 | "ts-gql-document-validator@v1,schema@" + config.schemaHash; 41 | 42 | export async function readDocumentValidationCache(config: Config) { 43 | let cacheFilename = nodePath.join( 44 | config.directory, 45 | "__generated__", 46 | "ts-gql", 47 | "@validation-cache.json" 48 | ); 49 | let cacheContents: string; 50 | try { 51 | cacheContents = await fs.readFile(cacheFilename, "utf8"); 52 | } catch (err) { 53 | if (err.code === "ENOENT") { 54 | return {}; 55 | } 56 | throw err; 57 | } 58 | if (!integrity.verify(cacheContents)) { 59 | return {}; 60 | } 61 | let parsed: { 62 | version: string; 63 | integrity: string; 64 | cache: DocumentValidationCache; 65 | } = JSON.parse(cacheContents); 66 | if (parsed.version !== getDocumentValidationCacheVersion(config)) { 67 | return {}; 68 | } 69 | return parsed.cache; 70 | } 71 | 72 | export function writeDocumentValidationCache( 73 | config: Config, 74 | cache: DocumentValidationCache 75 | ) { 76 | let cacheFilename = nodePath.join( 77 | config.directory, 78 | "__generated__", 79 | "ts-gql", 80 | "@validation-cache.json" 81 | ); 82 | let stringified = JSON.stringify( 83 | { 84 | version: getDocumentValidationCacheVersion(config), 85 | integrity: integrity.placeholder, 86 | cache, 87 | }, 88 | null, 89 | 2 90 | ); 91 | let signed = integrity.sign(stringified); 92 | return fs.writeFile(cacheFilename, signed); 93 | } 94 | 95 | let rules: { operation: ValidationRule[]; fragment: ValidationRule[] }; 96 | 97 | export function validateDocument( 98 | document: DocumentNode, 99 | filename: string, 100 | config: Config, 101 | loc: FullSourceLocation 102 | ) { 103 | const { specifiedRules, NoUnusedFragmentsRule, validate } = 104 | lazyRequire(); 105 | 106 | if (!rules) { 107 | rules = { 108 | fragment: [SkipNonFirstFragmentsRule, FragmentNameValidationRule].concat( 109 | specifiedRules.filter((x) => x !== NoUnusedFragmentsRule) 110 | ), 111 | operation: [SkipNonFirstFragmentsRule].concat(specifiedRules), 112 | }; 113 | } 114 | 115 | return validate( 116 | config.schema(), 117 | document, 118 | document.definitions[0].kind === "OperationDefinition" 119 | ? rules.operation 120 | : rules.fragment 121 | ).map((err) => ({ 122 | filename, 123 | message: err.message, 124 | loc: locFromSourceAndGraphQLError(loc, err), 125 | })); 126 | } 127 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/README.md: -------------------------------------------------------------------------------- 1 | These packages are inlined from https://github.com/dotansimha/graphql-code-generator, mainly to remove the dependency on relay-compiler which adds quite a large dependency tree 2 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/auto-bind.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/sindresorhus/auto-bind/blob/5f9859a2c163a6567f0595b6268ef667c8248c6a/index.js 2 | const getAllProperties = (object: object) => { 3 | const properties = new Set<[Record, string | symbol]>(); 4 | 5 | do { 6 | for (const key of Reflect.ownKeys(object)) { 7 | properties.add([object, key]); 8 | } 9 | } while ( 10 | (object = Reflect.getPrototypeOf(object)!) && 11 | object !== Object.prototype 12 | ); 13 | 14 | return properties; 15 | }; 16 | 17 | export function autoBind>( 18 | self: SelfType 19 | ): SelfType { 20 | for (const [object, key] of getAllProperties(self.constructor.prototype)) { 21 | if (key === "constructor") { 22 | continue; 23 | } 24 | 25 | const descriptor = Reflect.getOwnPropertyDescriptor(object, key); 26 | if (descriptor && typeof descriptor.value === "function") { 27 | (self as any)[key] = self[key as string].bind(self); 28 | } 29 | } 30 | 31 | return self; 32 | } 33 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/codegen-core/execute-plugin.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/dotansimha/graphql-code-generator/blob/master/packages/graphql-codegen-core/src/execute-plugin.ts 2 | 3 | import type { Types, CodegenPlugin } from "@graphql-codegen/plugin-helpers"; 4 | import type { GraphQLSchema } from "graphql"; 5 | 6 | export interface ExecutePluginOptions { 7 | name: string; 8 | config: Types.PluginConfig; 9 | parentConfig: Types.PluginConfig; 10 | schemaAst: GraphQLSchema; 11 | documents: Types.DocumentFile[]; 12 | outputFilename: string; 13 | allPlugins: Types.ConfiguredPlugin[]; 14 | skipDocumentsValidation?: boolean; 15 | } 16 | 17 | export function executePlugin( 18 | options: ExecutePluginOptions, 19 | plugin: CodegenPlugin 20 | ): Types.PluginOutput { 21 | const outputSchema: GraphQLSchema = options.schemaAst; 22 | const documents = options.documents || []; 23 | 24 | let result = plugin.plugin( 25 | outputSchema, 26 | documents, 27 | typeof options.config === "object" ? { ...options.config } : options.config, 28 | { 29 | outputFile: options.outputFilename, 30 | allPlugins: options.allPlugins, 31 | } 32 | ); 33 | // @ts-ignore 34 | if (typeof result.then === "function") { 35 | throw new Error("plugins must be synchronous"); 36 | } 37 | return result as any as Types.PluginOutput; 38 | } 39 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/codegen-core/index.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/dotansimha/graphql-code-generator/blob/master/packages/graphql-codegen-core/src/codegen.ts 2 | 3 | import type { Types, CodegenPlugin } from "@graphql-codegen/plugin-helpers"; 4 | import { GraphQLSchema } from "graphql/type"; 5 | import { executePlugin } from "./execute-plugin"; 6 | 7 | function isComplexPluginOutput(obj: Types.PluginOutput) { 8 | return typeof obj === "object" && obj.hasOwnProperty("content"); 9 | } 10 | 11 | interface GenerateOptions { 12 | filename: string; 13 | plugins: Types.ConfiguredPlugin[]; 14 | schemaAst: GraphQLSchema; 15 | documents: Types.DocumentFile[]; 16 | config: { 17 | [key: string]: any; 18 | }; 19 | pluginMap: { 20 | [name: string]: CodegenPlugin; 21 | }; 22 | skipDocumentsValidation?: boolean; 23 | } 24 | 25 | export function codegen(options: GenerateOptions): string { 26 | const prepend: Set = new Set(); 27 | const append: Set = new Set(); 28 | 29 | const output = options.plugins.map((plugin) => { 30 | const name = Object.keys(plugin)[0]; 31 | const pluginPackage = options.pluginMap[name]; 32 | const pluginConfig = plugin[name] || {}; 33 | 34 | const execConfig = 35 | typeof pluginConfig !== "object" 36 | ? pluginConfig 37 | : { 38 | ...options.config, 39 | ...pluginConfig, 40 | }; 41 | 42 | const result = executePlugin( 43 | { 44 | name, 45 | config: execConfig, 46 | parentConfig: options.config, 47 | schemaAst: options.schemaAst, 48 | documents: options.documents, 49 | outputFilename: options.filename, 50 | allPlugins: options.plugins, 51 | skipDocumentsValidation: options.skipDocumentsValidation, 52 | }, 53 | pluginPackage 54 | ); 55 | 56 | if (typeof result === "string") { 57 | return result || ""; 58 | } else if (isComplexPluginOutput(result)) { 59 | if (result.append && result.append.length > 0) { 60 | for (const item of result.append) { 61 | append.add(item); 62 | } 63 | } 64 | 65 | if (result.prepend && result.prepend.length > 0) { 66 | for (const item of result.prepend) { 67 | prepend.add(item); 68 | } 69 | } 70 | return result.content || ""; 71 | } 72 | 73 | return ""; 74 | }); 75 | 76 | return [ 77 | ...sortPrependValues(Array.from(prepend.values())), 78 | ...output, 79 | ...Array.from(append.values()), 80 | ].join("\n"); 81 | } 82 | 83 | function resolveCompareValue(a: string) { 84 | if ( 85 | a.startsWith("/*") || 86 | a.startsWith("//") || 87 | a.startsWith(" *") || 88 | a.startsWith(" */") || 89 | a.startsWith("*/") 90 | ) { 91 | return 0; 92 | } else if (a.startsWith("package")) { 93 | return 1; 94 | } else if (a.startsWith("import")) { 95 | return 2; 96 | } else { 97 | return 3; 98 | } 99 | } 100 | 101 | export function sortPrependValues(values: string[]): string[] { 102 | return values.sort((a: string, b: string) => { 103 | const aV = resolveCompareValue(a); 104 | const bV = resolveCompareValue(b); 105 | 106 | if (aV < bV) { 107 | return -1; 108 | } 109 | if (aV > bV) { 110 | return 1; 111 | } 112 | 113 | return 0; 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/typescript-operations/config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AvoidOptionalsConfig, 3 | RawDocumentsConfig, 4 | } from "../visitor-plugin-common"; 5 | 6 | /** 7 | * @description This plugin generates TypeScript types based on your GraphQLSchema _and_ your GraphQL operations and fragments. 8 | * It generates types for your GraphQL documents: Query, Mutation, Subscription and Fragment. 9 | * 10 | * Note: In most configurations, this plugin requires you to use `typescript as well, because it depends on its base types. 11 | */ 12 | export interface TypeScriptDocumentsPluginConfig extends RawDocumentsConfig { 13 | /** 14 | * @description The [GraphQL spec](https://spec.graphql.org/draft/#sel-FAHjBJFCAACE_Gh7d) 15 | * allows arrays and a single primitive value for list input. This allows to 16 | * deactivate that behavior to only accept arrays instead of single values. If 17 | * set to `false`, the definition: `query foo(bar: [Int!]!): Foo` will output 18 | * `bar: Array` instead of `bar: Array | Int` for the variable part. 19 | * @default true 20 | * 21 | * @exampleMarkdown 22 | * ```ts filename="codegen.ts" 23 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 24 | * 25 | * const config: CodegenConfig = { 26 | * // ... 27 | * generates: { 28 | * 'path/to/file.ts': { 29 | * plugins: ['typescript'], 30 | * config: { 31 | * arrayInputCoercion: false 32 | * }, 33 | * }, 34 | * }, 35 | * }; 36 | * export default config; 37 | * ``` 38 | */ 39 | arrayInputCoercion?: boolean; 40 | /** 41 | * @description This will cause the generator to avoid using TypeScript optionals (`?`) on types, 42 | * so the following definition: `type A { myField: String }` will output `myField: Maybe` 43 | * instead of `myField?: Maybe`. 44 | * @default false 45 | * 46 | * @exampleMarkdown 47 | * ## Override all definition types 48 | * 49 | * ```ts filename="codegen.ts" 50 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 51 | * 52 | * const config: CodegenConfig = { 53 | * // ... 54 | * generates: { 55 | * 'path/to/file.ts': { 56 | * plugins: ['typescript'], 57 | * config: { 58 | * avoidOptionals: true 59 | * }, 60 | * }, 61 | * }, 62 | * }; 63 | * export default config; 64 | * ``` 65 | * 66 | * ## Override only specific definition types 67 | * 68 | * ```ts filename="codegen.ts" 69 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 70 | * 71 | * const config: CodegenConfig = { 72 | * // ... 73 | * generates: { 74 | * 'path/to/file.ts': { 75 | * plugins: ['typescript'], 76 | * config: { 77 | * avoidOptionals: { 78 | * field: true 79 | * inputValue: true 80 | * object: true 81 | * defaultValue: true 82 | * } 83 | * }, 84 | * }, 85 | * }, 86 | * }; 87 | * export default config; 88 | * ``` 89 | */ 90 | avoidOptionals?: boolean | AvoidOptionalsConfig; 91 | /** 92 | * @description Generates immutable types by adding `readonly` to properties and uses `ReadonlyArray`. 93 | * @default false 94 | * 95 | * @exampleMarkdown 96 | * ```ts filename="codegen.ts" 97 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 98 | * 99 | * const config: CodegenConfig = { 100 | * // ... 101 | * generates: { 102 | * 'path/to/file.ts': { 103 | * plugins: ['typescript'], 104 | * config: { 105 | * immutableTypes: true 106 | * }, 107 | * }, 108 | * }, 109 | * }; 110 | * export default config; 111 | * ``` 112 | */ 113 | immutableTypes?: boolean; 114 | 115 | /** 116 | * @description Set to `true` in order to generate output without `export` modifier. 117 | * This is useful if you are generating `.d.ts` file and want it to be globally available. 118 | * @default false 119 | * 120 | * @exampleMarkdown 121 | * ```ts filename="codegen.ts" 122 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 123 | * 124 | * const config: CodegenConfig = { 125 | * // ... 126 | * generates: { 127 | * 'path/to/file.ts': { 128 | * plugins: ['typescript'], 129 | * config: { 130 | * noExport: true 131 | * }, 132 | * }, 133 | * }, 134 | * }; 135 | * export default config; 136 | * ``` 137 | */ 138 | noExport?: boolean; 139 | /** 140 | * @description Allow to override the type value of `Maybe`. 141 | * @default T | null 142 | * 143 | * @exampleMarkdown 144 | * ## Allow undefined 145 | * ```ts filename="codegen.ts" 146 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 147 | * 148 | * const config: CodegenConfig = { 149 | * // ... 150 | * generates: { 151 | * 'path/to/file.ts': { 152 | * plugins: ['typescript'], 153 | * config: { 154 | * maybeValue: 'T | null | undefined' 155 | * }, 156 | * }, 157 | * }, 158 | * }; 159 | * export default config; 160 | * ``` 161 | * 162 | * ## Allow `null` in resolvers: 163 | * ```ts filename="codegen.ts" 164 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 165 | * 166 | * const config: CodegenConfig = { 167 | * // ... 168 | * generates: { 169 | * 'path/to/file.ts': { 170 | * plugins: ['typescript'], 171 | * config: { 172 | * maybeValue: 'T extends PromiseLike ? Promise : T | null' 173 | * }, 174 | * }, 175 | * }, 176 | * }; 177 | * export default config; 178 | * ``` 179 | */ 180 | maybeValue?: string; 181 | 182 | /** 183 | * @description Adds undefined as a possible type for query variables 184 | * @default false 185 | * 186 | * @exampleMarkdown 187 | * ```ts filename="codegen.ts" 188 | * import type { CodegenConfig } from '@graphql-codegen/cli'; 189 | * 190 | * const config: CodegenConfig = { 191 | * // ... 192 | * generates: { 193 | * 'path/to/file.ts': { 194 | * plugins: ['typescript'], 195 | * config: { 196 | * allowUndefinedQueryVariables: true 197 | * }, 198 | * }, 199 | * }, 200 | * }; 201 | * export default config; 202 | * ``` 203 | */ 204 | 205 | allowUndefinedQueryVariables?: boolean; 206 | } 207 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/typescript-operations/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginFunction, Types } from "@graphql-codegen/plugin-helpers"; 2 | import { LoadedFragment } from "../visitor-plugin-common"; 3 | import type { FragmentDefinitionNode, GraphQLSchema } from "graphql"; 4 | import { Kind } from "graphql/language/kinds"; 5 | import { concatAST } from "graphql/utilities/concatAST"; 6 | import { TypeScriptDocumentsPluginConfig } from "./config"; 7 | import { TypeScriptDocumentsVisitor } from "./visitor"; 8 | 9 | export type { TypeScriptDocumentsPluginConfig } from "./config"; 10 | 11 | import type { ASTNode } from "graphql"; 12 | import { visit } from "graphql/language/visitor"; 13 | 14 | type VisitFn = typeof visit; 15 | type NewVisitor = Partial[1]>; 16 | type OldVisitor = { 17 | enter?: Partial< 18 | Record["enter"]> 19 | >; 20 | leave?: Partial< 21 | Record["leave"]> 22 | >; 23 | } & NewVisitor; 24 | 25 | function oldVisit( 26 | root: ASTNode, 27 | { enter: enterVisitors, leave: leaveVisitors, ..._newVisitor }: OldVisitor 28 | ): any { 29 | const newVisitor: any = _newVisitor; 30 | if (typeof enterVisitors === "object") { 31 | for (const key in enterVisitors) { 32 | newVisitor[key] ||= {}; 33 | newVisitor[key].enter = (enterVisitors as any)[key]; 34 | } 35 | } 36 | if (typeof leaveVisitors === "object") { 37 | for (const key in leaveVisitors) { 38 | newVisitor[key] ||= {}; 39 | newVisitor[key].leave = (leaveVisitors as any)[key]; 40 | } 41 | } 42 | return visit(root, newVisitor); 43 | } 44 | 45 | export const typescriptOperationsPlugin: PluginFunction< 46 | TypeScriptDocumentsPluginConfig, 47 | Types.ComplexPluginOutput 48 | > = ( 49 | schema: GraphQLSchema, 50 | documents: Types.DocumentFile[], 51 | config: TypeScriptDocumentsPluginConfig 52 | ) => { 53 | const allAst = concatAST(documents.map((v) => v.document!)); 54 | 55 | const allFragments: LoadedFragment[] = [ 56 | ...( 57 | allAst.definitions.filter( 58 | (d) => d.kind === Kind.FRAGMENT_DEFINITION 59 | ) as FragmentDefinitionNode[] 60 | ).map((fragmentDef) => ({ 61 | node: fragmentDef, 62 | name: fragmentDef.name.value, 63 | onType: fragmentDef.typeCondition.name.value, 64 | isExternal: false, 65 | })), 66 | ]; 67 | 68 | const visitor = new TypeScriptDocumentsVisitor(schema, config, allFragments); 69 | 70 | const visitorResult = oldVisit(allAst, { 71 | leave: visitor as any, 72 | }); 73 | 74 | let content = visitorResult.definitions.join("\n"); 75 | 76 | return { 77 | prepend: [ 78 | ...visitor.getImports(), 79 | ...visitor.getGlobalDeclarations(visitor.config.noExport), 80 | ], 81 | content, 82 | }; 83 | }; 84 | 85 | export { TypeScriptDocumentsVisitor }; 86 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/typescript-operations/ts-operation-variables-to-object.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AvoidOptionalsConfig, 3 | ConvertNameFn, 4 | normalizeAvoidOptionals, 5 | NormalizedScalarsMap, 6 | OperationVariablesToObject, 7 | ParsedDirectiveArgumentAndInputFieldMappings, 8 | ParsedEnumValuesMap, 9 | } from "../visitor-plugin-common"; 10 | import type { TypeNode } from "graphql"; 11 | import { Kind } from "graphql/language/kinds"; 12 | 13 | export class TypeScriptOperationVariablesToObject extends OperationVariablesToObject { 14 | constructor( 15 | _scalars: NormalizedScalarsMap, 16 | _convertName: ConvertNameFn, 17 | private _avoidOptionals: boolean | AvoidOptionalsConfig, 18 | private _immutableTypes: boolean, 19 | _namespacedImportName: string | null = null, 20 | _enumNames: string[] = [], 21 | _enumPrefix = true, 22 | _enumSuffix = true, 23 | _enumValues: ParsedEnumValuesMap = {}, 24 | _applyCoercion: Boolean = false, 25 | _directiveArgumentAndInputFieldMappings: ParsedDirectiveArgumentAndInputFieldMappings = {}, 26 | private _maybeType = "Maybe" 27 | ) { 28 | super( 29 | _scalars, 30 | _convertName, 31 | _namespacedImportName, 32 | _enumNames, 33 | _enumPrefix, 34 | _enumSuffix, 35 | _enumValues, 36 | _applyCoercion, 37 | _directiveArgumentAndInputFieldMappings 38 | ); 39 | } 40 | 41 | private clearOptional(str: string): string { 42 | const prefix = this._namespacedImportName 43 | ? `${this._namespacedImportName}.` 44 | : ""; 45 | const rgx = new RegExp(`^${this.wrapMaybe(`(.*?)`)}$`, "i"); 46 | 47 | if (str.startsWith(`${prefix}${this._maybeType}`)) { 48 | return str.replace(rgx, "$1"); 49 | } 50 | 51 | return str; 52 | } 53 | 54 | public wrapAstTypeWithModifiers( 55 | baseType: string, 56 | typeNode: TypeNode, 57 | applyCoercion = false 58 | ): string { 59 | if (typeNode.kind === Kind.NON_NULL_TYPE) { 60 | const type = this.wrapAstTypeWithModifiers( 61 | baseType, 62 | typeNode.type, 63 | applyCoercion 64 | ); 65 | 66 | return this.clearOptional(type); 67 | } 68 | if (typeNode.kind === Kind.LIST_TYPE) { 69 | const innerType = this.wrapAstTypeWithModifiers( 70 | baseType, 71 | typeNode.type, 72 | applyCoercion 73 | ); 74 | const listInputCoercionExtension = applyCoercion ? ` | ${innerType}` : ""; 75 | 76 | return this.wrapMaybe( 77 | `${ 78 | this._immutableTypes ? "ReadonlyArray" : "Array" 79 | }<${innerType}>${listInputCoercionExtension}` 80 | ); 81 | } 82 | return this.wrapMaybe(baseType); 83 | } 84 | 85 | protected formatFieldString( 86 | fieldName: string, 87 | isNonNullType: boolean, 88 | hasDefaultValue: boolean 89 | ): string { 90 | return `${fieldName}${ 91 | this.getAvoidOption(isNonNullType, hasDefaultValue) ? "?" : "" 92 | }`; 93 | } 94 | 95 | protected formatTypeString(fieldType: string): string { 96 | return fieldType; 97 | } 98 | 99 | protected wrapMaybe(type?: string) { 100 | const prefix = this._namespacedImportName 101 | ? `${this._namespacedImportName}.` 102 | : ""; 103 | return `${prefix}${this._maybeType}${type ? `<${type}>` : ""}`; 104 | } 105 | 106 | protected getAvoidOption(isNonNullType: boolean, hasDefaultValue: boolean) { 107 | const options = normalizeAvoidOptionals(this._avoidOptionals); 108 | return ( 109 | ((options.object || !options.defaultValue) && hasDefaultValue) || 110 | (!options.object && !isNonNullType) 111 | ); 112 | } 113 | 114 | protected getPunctuation(): string { 115 | return ";"; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/typescript-operations/ts-selection-set-processor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseSelectionSetProcessor, 3 | LinkField, 4 | PrimitiveAliasedFields, 5 | PrimitiveField, 6 | ProcessResult, 7 | SelectionSetProcessorConfig, 8 | } from "../visitor-plugin-common"; 9 | import { 10 | GraphQLInterfaceType, 11 | GraphQLObjectType, 12 | } from "graphql/type/definition"; 13 | 14 | export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor { 15 | transformPrimitiveFields( 16 | schemaType: GraphQLObjectType | GraphQLInterfaceType, 17 | fields: PrimitiveField[], 18 | unsetTypes?: boolean 19 | ): ProcessResult { 20 | if (fields.length === 0) { 21 | return []; 22 | } 23 | 24 | const parentName = 25 | (this.config.namespacedImportName 26 | ? `${this.config.namespacedImportName}.` 27 | : "") + 28 | this.config.convertName(schemaType.name, { 29 | useTypesPrefix: true, 30 | }); 31 | 32 | if (unsetTypes) { 33 | return [ 34 | `MakeEmpty<${parentName}, ${fields 35 | .map((field) => `'${field.fieldName}'`) 36 | .join(" | ")}>`, 37 | ]; 38 | } 39 | 40 | let hasConditionals = false; 41 | const conditilnalsList: string[] = []; 42 | let resString = `Pick<${parentName}, ${fields 43 | .map((field) => { 44 | if (field.isConditional) { 45 | hasConditionals = true; 46 | conditilnalsList.push(field.fieldName); 47 | } 48 | return `'${field.fieldName}'`; 49 | }) 50 | .join(" | ")}>`; 51 | 52 | if (hasConditionals) { 53 | const avoidOptional = 54 | // TODO: check type and exec only if relevant 55 | this.config.avoidOptionals === true || 56 | (typeof this.config.avoidOptionals === "object" && 57 | (this.config.avoidOptionals.field || 58 | this.config.avoidOptionals.inputValue || 59 | this.config.avoidOptionals.object)); 60 | 61 | const transform = avoidOptional ? "MakeMaybe" : "MakeOptional"; 62 | resString = `${ 63 | this.config.namespacedImportName 64 | ? `${this.config.namespacedImportName}.` 65 | : "" 66 | }${transform}<${resString}, ${conditilnalsList 67 | .map((field) => `'${field}'`) 68 | .join(" | ")}>`; 69 | } 70 | return [resString]; 71 | } 72 | 73 | transformTypenameField(type: string, name: string): ProcessResult { 74 | return [`{ ${name}: ${type} }`]; 75 | } 76 | 77 | transformAliasesPrimitiveFields( 78 | schemaType: GraphQLObjectType | GraphQLInterfaceType, 79 | fields: PrimitiveAliasedFields[] 80 | ): ProcessResult { 81 | if (fields.length === 0) { 82 | return []; 83 | } 84 | 85 | const parentName = 86 | (this.config.namespacedImportName 87 | ? `${this.config.namespacedImportName}.` 88 | : "") + 89 | this.config.convertName(schemaType.name, { 90 | useTypesPrefix: true, 91 | }); 92 | 93 | return [ 94 | `{ ${fields 95 | .map((aliasedField) => { 96 | const value = 97 | aliasedField.fieldName === "__typename" 98 | ? `'${schemaType.name}'` 99 | : `${parentName}['${aliasedField.fieldName}']`; 100 | 101 | return `${aliasedField.alias}: ${value}`; 102 | }) 103 | .join(", ")} }`, 104 | ]; 105 | } 106 | 107 | transformLinkFields(fields: LinkField[]): ProcessResult { 108 | if (fields.length === 0) { 109 | return []; 110 | } 111 | 112 | return [ 113 | `{ ${fields 114 | .map((field) => `${field.alias || field.name}: ${field.selectionSet}`) 115 | .join(", ")} }`, 116 | ]; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/typescript-operations/visitor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AvoidOptionalsConfig, 3 | BaseDocumentsVisitor, 4 | DeclarationKind, 5 | generateFragmentImportStatement, 6 | getConfigValue, 7 | LoadedFragment, 8 | normalizeAvoidOptionals, 9 | ParsedDocumentsConfig, 10 | PreResolveTypesProcessor, 11 | SelectionSetProcessorConfig, 12 | SelectionSetToObject, 13 | wrapTypeWithModifiers, 14 | } from "../visitor-plugin-common"; 15 | import { autoBind } from "../auto-bind"; 16 | import { isEnumType, isNonNullType } from "graphql/type/definition"; 17 | import type { 18 | GraphQLNamedType, 19 | GraphQLOutputType, 20 | GraphQLSchema, 21 | } from "graphql"; 22 | import { TypeScriptDocumentsPluginConfig } from "./config"; 23 | import { TypeScriptOperationVariablesToObject } from "./ts-operation-variables-to-object"; 24 | import { TypeScriptSelectionSetProcessor } from "./ts-selection-set-processor"; 25 | 26 | export interface TypeScriptDocumentsParsedConfig extends ParsedDocumentsConfig { 27 | arrayInputCoercion: boolean; 28 | avoidOptionals: AvoidOptionalsConfig; 29 | immutableTypes: boolean; 30 | noExport: boolean; 31 | maybeValue: string; 32 | allowUndefinedQueryVariables: boolean; 33 | } 34 | 35 | export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor< 36 | TypeScriptDocumentsPluginConfig, 37 | TypeScriptDocumentsParsedConfig 38 | > { 39 | constructor( 40 | schema: GraphQLSchema, 41 | config: TypeScriptDocumentsPluginConfig, 42 | allFragments: LoadedFragment[] 43 | ) { 44 | super( 45 | config, 46 | { 47 | arrayInputCoercion: getConfigValue(config.arrayInputCoercion, true), 48 | noExport: getConfigValue(config.noExport, false), 49 | avoidOptionals: normalizeAvoidOptionals( 50 | getConfigValue(config.avoidOptionals, false) 51 | ), 52 | immutableTypes: getConfigValue(config.immutableTypes, false), 53 | nonOptionalTypename: getConfigValue(config.nonOptionalTypename, false), 54 | preResolveTypes: getConfigValue(config.preResolveTypes, true), 55 | mergeFragmentTypes: getConfigValue(config.mergeFragmentTypes, false), 56 | allowUndefinedQueryVariables: getConfigValue( 57 | config.allowUndefinedQueryVariables, 58 | false 59 | ), 60 | } as TypeScriptDocumentsParsedConfig, 61 | schema 62 | ); 63 | 64 | autoBind(this); 65 | 66 | const preResolveTypes = getConfigValue(config.preResolveTypes, true); 67 | const defaultMaybeValue = "T | null"; 68 | const maybeValue = getConfigValue(config.maybeValue, defaultMaybeValue); 69 | 70 | const wrapOptional = (type: string) => { 71 | if (preResolveTypes === true) { 72 | return maybeValue.replace("T", type); 73 | } 74 | const prefix = this.config.namespacedImportName 75 | ? `${this.config.namespacedImportName}.` 76 | : ""; 77 | return `${prefix}Maybe<${type}>`; 78 | }; 79 | const wrapArray = (type: string) => { 80 | const listModifier = this.config.immutableTypes 81 | ? "ReadonlyArray" 82 | : "Array"; 83 | return `${listModifier}<${type}>`; 84 | }; 85 | 86 | const formatNamedField = ( 87 | name: string, 88 | type: GraphQLOutputType | GraphQLNamedType | null, 89 | isConditional = false, 90 | isOptional = false 91 | ): string => { 92 | const optional = 93 | isOptional || 94 | isConditional || 95 | (!this.config.avoidOptionals.field && !!type && !isNonNullType(type)); 96 | return ( 97 | (this.config.immutableTypes ? `readonly ${name}` : name) + 98 | (optional ? "?" : "") 99 | ); 100 | }; 101 | 102 | const processorConfig: SelectionSetProcessorConfig = { 103 | namespacedImportName: this.config.namespacedImportName, 104 | convertName: this.convertName.bind(this), 105 | enumPrefix: this.config.enumPrefix, 106 | enumSuffix: this.config.enumSuffix, 107 | scalars: this.scalars, 108 | formatNamedField, 109 | wrapTypeWithModifiers(baseType, type) { 110 | return wrapTypeWithModifiers(baseType, type, { 111 | wrapOptional, 112 | wrapArray, 113 | }); 114 | }, 115 | avoidOptionals: this.config.avoidOptionals, 116 | printFieldsOnNewLines: this.config.printFieldsOnNewLines, 117 | }; 118 | const processor = new ( 119 | preResolveTypes 120 | ? PreResolveTypesProcessor 121 | : TypeScriptSelectionSetProcessor 122 | )(processorConfig); 123 | this.setSelectionSetHandler( 124 | new SelectionSetToObject( 125 | processor, 126 | this.scalars, 127 | this.schema, 128 | this.convertName.bind(this), 129 | this.getFragmentSuffix.bind(this), 130 | allFragments, 131 | this.config 132 | ) 133 | ); 134 | const enumsNames = Object.keys(schema.getTypeMap()).filter((typeName) => 135 | isEnumType(schema.getType(typeName)) 136 | ); 137 | this.setVariablesTransformer( 138 | new TypeScriptOperationVariablesToObject( 139 | this.scalars, 140 | this.convertName.bind(this), 141 | this.config.avoidOptionals.object!, 142 | this.config.immutableTypes, 143 | this.config.namespacedImportName, 144 | enumsNames, 145 | this.config.enumPrefix, 146 | this.config.enumSuffix, 147 | this.config.enumValues, 148 | this.config.arrayInputCoercion, 149 | undefined, 150 | "InputMaybe" 151 | ) 152 | ); 153 | this._declarationBlockConfig = { 154 | ignoreExport: this.config.noExport, 155 | }; 156 | } 157 | 158 | public getImports(): Array { 159 | return !this.config.globalNamespace && 160 | (this.config.inlineFragmentTypes === "combine" || 161 | this.config.inlineFragmentTypes === "mask") 162 | ? this.config.fragmentImports.map((fragmentImport) => 163 | generateFragmentImportStatement(fragmentImport, "type") 164 | ) 165 | : []; 166 | } 167 | 168 | protected getPunctuation(_declarationKind: DeclarationKind): string { 169 | return ";"; 170 | } 171 | 172 | protected applyVariablesWrapper( 173 | variablesBlock: string, 174 | operationType: string 175 | ): string { 176 | const prefix = this.config.namespacedImportName 177 | ? `${this.config.namespacedImportName}.` 178 | : ""; 179 | const extraType = 180 | this.config.allowUndefinedQueryVariables && operationType === "Query" 181 | ? " | undefined" 182 | : ""; 183 | 184 | return `${prefix}Exact<${ 185 | variablesBlock === "{}" ? `{ [key: string]: never; }` : variablesBlock 186 | }>${extraType}`; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/avoid-optionals.ts: -------------------------------------------------------------------------------- 1 | import { AvoidOptionalsConfig } from "./types"; 2 | 3 | export const DEFAULT_AVOID_OPTIONALS: AvoidOptionalsConfig = { 4 | object: false, 5 | inputValue: false, 6 | field: false, 7 | defaultValue: false, 8 | resolvers: false, 9 | }; 10 | 11 | export function normalizeAvoidOptionals( 12 | avoidOptionals?: boolean | AvoidOptionalsConfig 13 | ): AvoidOptionalsConfig { 14 | if (typeof avoidOptionals === "boolean") { 15 | return { 16 | object: avoidOptionals, 17 | inputValue: avoidOptionals, 18 | field: avoidOptionals, 19 | defaultValue: avoidOptionals, 20 | resolvers: avoidOptionals, 21 | }; 22 | } 23 | 24 | return { 25 | ...DEFAULT_AVOID_OPTIONALS, 26 | ...avoidOptionals, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/declaration-kinds.ts: -------------------------------------------------------------------------------- 1 | import { DeclarationKind, DeclarationKindConfig } from "./types"; 2 | 3 | export const DEFAULT_DECLARATION_KINDS: DeclarationKindConfig = { 4 | directive: "type", 5 | scalar: "type", 6 | input: "type", 7 | type: "type", 8 | interface: "type", 9 | arguments: "type", 10 | }; 11 | 12 | export function normalizeDeclarationKind( 13 | declarationKind?: DeclarationKind | DeclarationKindConfig 14 | ): DeclarationKindConfig { 15 | if (typeof declarationKind === "string") { 16 | return { 17 | directive: declarationKind, 18 | scalar: declarationKind, 19 | input: declarationKind, 20 | type: declarationKind, 21 | interface: declarationKind, 22 | arguments: declarationKind, 23 | }; 24 | } 25 | 26 | return { 27 | ...DEFAULT_DECLARATION_KINDS, 28 | ...declarationKind, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/imports.ts: -------------------------------------------------------------------------------- 1 | import { dirname, isAbsolute, join, relative, resolve, parse } from "path"; 2 | 3 | export type ImportDeclaration = { 4 | outputPath: string; 5 | importSource: ImportSource; 6 | baseOutputDir: string; 7 | baseDir: string; 8 | typesImport: boolean; 9 | emitLegacyCommonJSImports: boolean; 10 | }; 11 | 12 | export type ImportSource = { 13 | /** 14 | * Source path, relative to the `baseOutputDir` 15 | */ 16 | path: string; 17 | /** 18 | * Namespace to import source as 19 | */ 20 | namespace?: string; 21 | /** 22 | * Entity names to import 23 | */ 24 | identifiers?: T[]; 25 | }; 26 | 27 | export type FragmentImport = { 28 | name: string; 29 | kind: "type" | "document"; 30 | }; 31 | 32 | export function generateFragmentImportStatement( 33 | statement: ImportDeclaration, 34 | kind: "type" | "document" | "both" 35 | ): string { 36 | const { importSource: fragmentImportSource, ...rest } = statement; 37 | const { identifiers, path, namespace } = fragmentImportSource; 38 | const importSource: ImportSource = { 39 | identifiers: identifiers! 40 | .filter( 41 | (fragmentImport) => kind === "both" || kind === fragmentImport.kind 42 | ) 43 | .map(({ name }) => name), 44 | path, 45 | namespace, 46 | }; 47 | return generateImportStatement({ 48 | importSource, 49 | ...rest, 50 | typesImport: kind === "type" ? statement.typesImport : false, 51 | }); 52 | } 53 | 54 | export function generateImportStatement(statement: ImportDeclaration): string { 55 | const { baseDir, importSource, outputPath, typesImport } = statement; 56 | const importPath = resolveImportPath(baseDir, outputPath, importSource.path); 57 | const importNames = importSource.identifiers?.length 58 | ? `{ ${Array.from(new Set(importSource.identifiers)).join(", ")} }` 59 | : "*"; 60 | const importExtension = 61 | importPath.startsWith("/") || importPath.startsWith(".") 62 | ? statement.emitLegacyCommonJSImports 63 | ? "" 64 | : ".js" 65 | : ""; 66 | const importAlias = importSource.namespace 67 | ? ` as ${importSource.namespace}` 68 | : ""; 69 | const importStatement = typesImport ? "import type" : "import"; 70 | return `${importStatement} ${importNames}${importAlias} from '${importPath}${importExtension}';${ 71 | importAlias ? "\n" : "" 72 | }`; 73 | // return `${importStatement} ${importNames}${importAlias} from '${importPath}';${importAlias ? '\n' : ''}`; 74 | } 75 | 76 | function resolveImportPath( 77 | baseDir: string, 78 | outputPath: string, 79 | sourcePath: string 80 | ) { 81 | const shouldAbsolute = !sourcePath.startsWith("~"); 82 | if (shouldAbsolute) { 83 | const absGeneratedFilePath = resolve(baseDir, outputPath); 84 | const absImportFilePath = resolve(baseDir, sourcePath); 85 | return resolveRelativeImport(absGeneratedFilePath, absImportFilePath); 86 | } 87 | return sourcePath.replace(`~`, ""); 88 | } 89 | 90 | export function resolveRelativeImport(from: string, to: string): string { 91 | if (!isAbsolute(from)) { 92 | throw new Error( 93 | `Argument 'from' must be an absolute path, '${from}' given.` 94 | ); 95 | } 96 | if (!isAbsolute(to)) { 97 | throw new Error(`Argument 'to' must be an absolute path, '${to}' given.`); 98 | } 99 | return fixLocalFilePath(clearExtension(relative(dirname(from), to))); 100 | } 101 | 102 | export function resolveImportSource( 103 | source: string | ImportSource 104 | ): ImportSource { 105 | return typeof source === "string" ? { path: source } : source; 106 | } 107 | 108 | export function clearExtension(path: string): string { 109 | const parsedPath = parse(path); 110 | return join(parsedPath.dir, parsedPath.name).replace(/\\/g, "/"); 111 | } 112 | 113 | export function fixLocalFilePath(path: string): string { 114 | return path.startsWith("..") ? path : `./${path}`; 115 | } 116 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./avoid-optionals"; 2 | export * from "./base-documents-visitor"; 3 | export * from "./base-visitor"; 4 | export * from "./declaration-kinds"; 5 | export * from "./imports"; 6 | export * from "./mappers"; 7 | export * from "./naming"; 8 | export * from "./scalars"; 9 | export * from "./selection-set-processor/base"; 10 | export * from "./selection-set-processor/pre-resolve-types"; 11 | export * from "./selection-set-to-object"; 12 | export * from "./types"; 13 | export * from "./utils"; 14 | export * from "./variables-to-object"; 15 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/mappers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-inner-declarations */ 2 | import { 3 | DirectiveArgumentAndInputFieldMappings, 4 | ParsedDirectiveArgumentAndInputFieldMappings, 5 | } from "./types"; 6 | 7 | export type ParsedMapper = InternalParsedMapper | ExternalParsedMapper; 8 | export interface InternalParsedMapper { 9 | isExternal: false; 10 | type: string; 11 | } 12 | export interface ExternalParsedMapper { 13 | isExternal: true; 14 | type: string; 15 | import: string; 16 | source: string; 17 | default: boolean; 18 | } 19 | 20 | export function isExternalMapperType( 21 | m: ParsedMapper 22 | ): m is ExternalParsedMapper { 23 | return !!(m as ExternalParsedMapper).import; 24 | } 25 | 26 | enum MapperKind { 27 | Namespace, 28 | Default, 29 | Regular, 30 | } 31 | 32 | interface Helpers { 33 | items: string[]; 34 | isNamespace: boolean; 35 | isDefault: boolean; 36 | hasAlias: boolean; 37 | } 38 | 39 | function prepareLegacy(mapper: string): Helpers { 40 | const isScoped = mapper.includes("\\#"); 41 | if (mapper.includes("\\#")) { 42 | mapper = mapper.replace("\\#", ""); 43 | } 44 | const items = mapper.split("#"); 45 | const isNamespace = items.length === 3; 46 | const isDefault = 47 | items[1].trim() === "default" || items[1].startsWith("default "); 48 | const hasAlias = items[1].includes(" as "); 49 | const source = isScoped ? `#${items[0]}` : items[0]; 50 | items[0] = source; 51 | 52 | return { 53 | items, 54 | isDefault, 55 | isNamespace, 56 | hasAlias, 57 | }; 58 | } 59 | 60 | function prepare(mapper: string): Helpers { 61 | const isScoped = mapper.includes("\\#"); 62 | if (mapper.includes("\\#")) { 63 | mapper = mapper.replace("\\#", ""); 64 | } 65 | let [source, path] = mapper.split("#"); 66 | const isNamespace = path.includes("."); 67 | const isDefault = path.trim() === "default" || path.startsWith("default "); 68 | const hasAlias = path.includes(" as "); 69 | source = isScoped ? `#${source}` : source; 70 | 71 | return { 72 | items: isNamespace ? [source, ...path.split(".")] : [source, path], 73 | isDefault, 74 | isNamespace, 75 | hasAlias, 76 | }; 77 | } 78 | 79 | function isLegacyMode(mapper: string) { 80 | if (mapper.includes("\\#")) { 81 | mapper = mapper.replace("\\#", ""); 82 | } 83 | return mapper.split("#").length === 3; 84 | } 85 | 86 | export function parseMapper( 87 | mapper: string, 88 | gqlTypeName: string | null = null, 89 | suffix?: string 90 | ): ParsedMapper { 91 | if (isExternalMapper(mapper)) { 92 | const { isNamespace, isDefault, hasAlias, items } = isLegacyMode(mapper) 93 | ? prepareLegacy(mapper) 94 | : prepare(mapper); 95 | 96 | const mapperKind: MapperKind = isNamespace 97 | ? MapperKind.Namespace 98 | : isDefault 99 | ? MapperKind.Default 100 | : MapperKind.Regular; 101 | 102 | function handleAlias(isDefault = false) { 103 | const [importedType, aliasType] = items[1].split(/\s+as\s+/); 104 | 105 | const type = maybeSuffix(aliasType); 106 | 107 | return { 108 | importElement: isDefault ? type : `${importedType} as ${type}`, 109 | type, 110 | }; 111 | } 112 | 113 | function maybeSuffix(type: string) { 114 | if (suffix) { 115 | return addSuffix(type, suffix); 116 | } 117 | 118 | return type; 119 | } 120 | 121 | function handle(): { 122 | importElement: string; 123 | type: string; 124 | } { 125 | switch (mapperKind) { 126 | // ./my/module#Namespace#Identifier 127 | case MapperKind.Namespace: { 128 | const [, ns, identifier] = items; 129 | 130 | return { 131 | type: `${ns}.${identifier}`, 132 | importElement: ns, 133 | }; 134 | } 135 | 136 | case MapperKind.Default: { 137 | // ./my/module#default as alias 138 | if (hasAlias) { 139 | return handleAlias(true); 140 | } 141 | 142 | const type = maybeSuffix(String(gqlTypeName)); 143 | 144 | // ./my/module#default 145 | return { 146 | importElement: type, 147 | type, 148 | }; 149 | } 150 | 151 | case MapperKind.Regular: { 152 | // ./my/module#Identifier as alias 153 | if (hasAlias) { 154 | return handleAlias(); 155 | } 156 | 157 | const identifier = items[1]; 158 | 159 | const type = maybeSuffix(identifier); 160 | 161 | // ./my/module#Identifier 162 | return { 163 | type, 164 | importElement: suffix ? `${identifier} as ${type}` : type, 165 | }; 166 | } 167 | } 168 | } 169 | 170 | const { type, importElement } = handle(); 171 | 172 | return { 173 | default: isDefault, 174 | isExternal: true, 175 | source: items[0], 176 | type, 177 | import: importElement.replace(/<(.*?)>/g, ""), 178 | }; 179 | } 180 | 181 | return { 182 | isExternal: false, 183 | type: mapper, 184 | }; 185 | } 186 | 187 | function addSuffix(element: string, suffix: string): string { 188 | const generic = element.indexOf("<"); 189 | if (generic === -1) { 190 | return `${element}${suffix}`; 191 | } 192 | return `${element.slice(0, generic)}${suffix}${element.slice(generic)}`; 193 | } 194 | 195 | export function isExternalMapper(value: string): boolean { 196 | return value.includes("#"); 197 | } 198 | 199 | export function transformDirectiveArgumentAndInputFieldMappings( 200 | rawDirectiveArgumentAndInputFieldMappings: DirectiveArgumentAndInputFieldMappings, 201 | directiveArgumentAndInputFieldMappingTypeSuffix?: string 202 | ): ParsedDirectiveArgumentAndInputFieldMappings { 203 | const result: ParsedDirectiveArgumentAndInputFieldMappings = {}; 204 | 205 | for (const directive of Object.keys( 206 | rawDirectiveArgumentAndInputFieldMappings 207 | )) { 208 | const mapperDef = rawDirectiveArgumentAndInputFieldMappings[directive]; 209 | const parsedMapper = parseMapper( 210 | mapperDef, 211 | directive, 212 | directiveArgumentAndInputFieldMappingTypeSuffix 213 | ); 214 | result[directive] = parsedMapper; 215 | } 216 | 217 | return result; 218 | } 219 | 220 | export function buildMapperImport( 221 | source: string, 222 | types: { identifier: string; asDefault?: boolean }[], 223 | useTypeImports: boolean 224 | ): string | null { 225 | if (!types || types.length === 0) { 226 | return null; 227 | } 228 | 229 | const defaultType = types.find((t) => t.asDefault === true); 230 | let namedTypes = types.filter((t) => !t.asDefault); 231 | 232 | if (useTypeImports) { 233 | if (defaultType) { 234 | // default as Baz 235 | namedTypes = [ 236 | { identifier: `default as ${defaultType.identifier}` }, 237 | ...namedTypes, 238 | ]; 239 | } 240 | // { Foo, Bar as BarModel } 241 | const namedImports = namedTypes.length 242 | ? `{ ${namedTypes.map((t) => t.identifier).join(", ")} }` 243 | : ""; 244 | 245 | // { default as Baz, Foo, Bar as BarModel } 246 | return `import type ${[namedImports] 247 | .filter(Boolean) 248 | .join(", ")} from '${source}';`; 249 | } 250 | 251 | // { Foo, Bar as BarModel } 252 | const namedImports = namedTypes.length 253 | ? `{ ${namedTypes.map((t) => t.identifier).join(", ")} }` 254 | : ""; 255 | // Baz 256 | const defaultImport = defaultType ? defaultType.identifier : ""; 257 | 258 | // Baz, { Foo, Bar as BarModel } 259 | return `import ${[defaultImport, namedImports] 260 | .filter(Boolean) 261 | .join(", ")} from '${source}';`; 262 | } 263 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/naming.ts: -------------------------------------------------------------------------------- 1 | import type { ASTNode } from "graphql"; 2 | import { ConvertFn } from "./types"; 3 | 4 | function getName(node: ASTNode | string | undefined): string | undefined { 5 | if (node == null) { 6 | return undefined; 7 | } 8 | 9 | if (typeof node === "string") { 10 | return node; 11 | } 12 | 13 | switch (node.kind) { 14 | case "OperationDefinition": 15 | case "Variable": 16 | case "Argument": 17 | case "FragmentSpread": 18 | case "FragmentDefinition": 19 | case "ObjectField": 20 | case "Directive": 21 | case "NamedType": 22 | case "ScalarTypeDefinition": 23 | case "ObjectTypeDefinition": 24 | case "FieldDefinition": 25 | case "InputValueDefinition": 26 | case "InterfaceTypeDefinition": 27 | case "UnionTypeDefinition": 28 | case "EnumTypeDefinition": 29 | case "EnumValueDefinition": 30 | case "InputObjectTypeDefinition": 31 | case "DirectiveDefinition": { 32 | return getName(node.name); 33 | } 34 | case "Name": { 35 | return node.value; 36 | } 37 | case "Field": { 38 | return getName(node.alias || node.name); 39 | } 40 | case "VariableDefinition": { 41 | return getName(node.variable); 42 | } 43 | } 44 | 45 | return undefined; 46 | } 47 | 48 | export const convertName: ConvertFn = (node, opts) => { 49 | const prefix = opts?.prefix; 50 | const suffix = opts?.suffix; 51 | 52 | const str = [prefix || "", getName(node), suffix || ""].join(""); 53 | 54 | return str; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/scalars.ts: -------------------------------------------------------------------------------- 1 | import { NormalizedScalarsMap } from "./types"; 2 | 3 | export const DEFAULT_SCALARS: NormalizedScalarsMap = { 4 | ID: { 5 | input: "string", 6 | output: "string", 7 | }, 8 | String: { 9 | input: "string", 10 | output: "string", 11 | }, 12 | Boolean: { 13 | input: "boolean", 14 | output: "boolean", 15 | }, 16 | Int: { 17 | input: "number", 18 | output: "number", 19 | }, 20 | Float: { 21 | input: "number", 22 | output: "number", 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/selection-set-processor/base.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GraphQLInterfaceType, 3 | GraphQLNamedType, 4 | GraphQLObjectType, 5 | GraphQLOutputType, 6 | Location, 7 | } from "graphql"; 8 | import { 9 | AvoidOptionalsConfig, 10 | ConvertNameFn, 11 | NormalizedScalarsMap, 12 | } from "../types"; 13 | 14 | export type PrimitiveField = { isConditional: boolean; fieldName: string }; 15 | export type PrimitiveAliasedFields = { 16 | isConditional: boolean; 17 | alias: string; 18 | fieldName: string; 19 | }; 20 | export type LinkField = { 21 | alias: string; 22 | name: string; 23 | type: string; 24 | selectionSet: string; 25 | }; 26 | export type NameAndType = { name: string; type: string }; 27 | export type ProcessResult = null | Array; 28 | 29 | export type SelectionSetProcessorConfig = { 30 | namespacedImportName: string | null; 31 | convertName: ConvertNameFn; 32 | enumPrefix: boolean | null; 33 | enumSuffix: boolean | null; 34 | scalars: NormalizedScalarsMap; 35 | formatNamedField( 36 | name: string, 37 | type?: GraphQLOutputType | GraphQLNamedType | null, 38 | isConditional?: boolean, 39 | isOptional?: boolean 40 | ): string; 41 | wrapTypeWithModifiers( 42 | baseType: string, 43 | type: GraphQLOutputType | GraphQLNamedType 44 | ): string; 45 | avoidOptionals?: AvoidOptionalsConfig | boolean; 46 | printFieldsOnNewLines?: boolean; 47 | }; 48 | 49 | export class BaseSelectionSetProcessor< 50 | Config extends SelectionSetProcessorConfig 51 | > { 52 | typeCache = new Map>(); 53 | 54 | constructor(public config: Config) {} 55 | 56 | buildFieldsIntoObject(allObjectsMerged: string[]): string { 57 | if (this.config.printFieldsOnNewLines) { 58 | return `{\n ${allObjectsMerged.join(",\n ")}\n}`; 59 | } 60 | return `{ ${allObjectsMerged.join(", ")} }`; 61 | } 62 | 63 | buildSelectionSetFromStrings(pieces: string[]): string { 64 | if (pieces.length === 0) { 65 | return null!; 66 | } 67 | if (pieces.length === 1) { 68 | return pieces[0]; 69 | } 70 | return `(\n ${pieces.join(`\n & `)}\n)`; 71 | } 72 | 73 | transformPrimitiveFields( 74 | _schemaType: GraphQLObjectType | GraphQLInterfaceType, 75 | _fields: PrimitiveField[], 76 | _unsetTypes?: boolean 77 | ): ProcessResult { 78 | throw new Error( 79 | `Please override "transformPrimitiveFields" as part of your BaseSelectionSetProcessor implementation!` 80 | ); 81 | } 82 | 83 | transformAliasesPrimitiveFields( 84 | _schemaType: GraphQLObjectType | GraphQLInterfaceType, 85 | _fields: PrimitiveAliasedFields[], 86 | _unsetTypes?: boolean 87 | ): ProcessResult { 88 | throw new Error( 89 | `Please override "transformAliasesPrimitiveFields" as part of your BaseSelectionSetProcessor implementation!` 90 | ); 91 | } 92 | 93 | transformLinkFields( 94 | _fields: LinkField[], 95 | _unsetTypes?: boolean 96 | ): ProcessResult { 97 | throw new Error( 98 | `Please override "transformLinkFields" as part of your BaseSelectionSetProcessor implementation!` 99 | ); 100 | } 101 | 102 | transformTypenameField(_type: string, _name: string): ProcessResult { 103 | throw new Error( 104 | `Please override "transformTypenameField" as part of your BaseSelectionSetProcessor implementation!` 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/selection-set-processor/pre-resolve-types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInterfaceType, 3 | GraphQLObjectType, 4 | isEnumType, 5 | isNonNullType, 6 | } from "graphql/type/definition"; 7 | import { 8 | BaseSelectionSetProcessor, 9 | LinkField, 10 | PrimitiveAliasedFields, 11 | PrimitiveField, 12 | ProcessResult, 13 | SelectionSetProcessorConfig, 14 | } from "./base"; 15 | import { getBaseType } from "../utils"; 16 | 17 | export class PreResolveTypesProcessor extends BaseSelectionSetProcessor { 18 | transformTypenameField(type: string, name: string): ProcessResult { 19 | return [ 20 | { 21 | type, 22 | name, 23 | }, 24 | ]; 25 | } 26 | 27 | transformPrimitiveFields( 28 | schemaType: GraphQLObjectType | GraphQLInterfaceType, 29 | fields: PrimitiveField[], 30 | unsetTypes?: boolean 31 | ): ProcessResult { 32 | if (fields.length === 0) { 33 | return []; 34 | } 35 | 36 | return fields.map((field) => { 37 | const fieldObj = schemaType.getFields()[field.fieldName]; 38 | 39 | const baseType = getBaseType(fieldObj.type); 40 | let typeToUse = baseType.name; 41 | 42 | const innerType = 43 | field.isConditional && isNonNullType(fieldObj.type) 44 | ? fieldObj.type.ofType 45 | : fieldObj.type; 46 | 47 | const name = this.config.formatNamedField( 48 | field.fieldName, 49 | innerType, 50 | field.isConditional, 51 | unsetTypes 52 | ); 53 | 54 | if (unsetTypes) { 55 | return { 56 | name, 57 | type: "never", 58 | }; 59 | } 60 | 61 | if (isEnumType(baseType)) { 62 | typeToUse = 63 | (this.config.namespacedImportName 64 | ? `${this.config.namespacedImportName}.` 65 | : "") + 66 | this.config.convertName(baseType.name, { 67 | useTypesPrefix: this.config.enumPrefix, 68 | useTypesSuffix: this.config.enumSuffix, 69 | }); 70 | } else if (this.config.scalars[baseType.name]) { 71 | typeToUse = this.config.scalars[baseType.name].output; 72 | } 73 | 74 | const wrappedType = this.config.wrapTypeWithModifiers( 75 | typeToUse, 76 | fieldObj.type 77 | ); 78 | 79 | return { 80 | name, 81 | type: wrappedType, 82 | }; 83 | }); 84 | } 85 | 86 | transformAliasesPrimitiveFields( 87 | schemaType: GraphQLObjectType | GraphQLInterfaceType, 88 | fields: PrimitiveAliasedFields[], 89 | unsetTypes?: boolean 90 | ): ProcessResult { 91 | if (fields.length === 0) { 92 | return []; 93 | } 94 | 95 | return fields.map((aliasedField) => { 96 | if (aliasedField.fieldName === "__typename") { 97 | const name = this.config.formatNamedField(aliasedField.alias, null); 98 | return { 99 | name, 100 | type: `'${schemaType.name}'`, 101 | }; 102 | } 103 | const fieldObj = schemaType.getFields()[aliasedField.fieldName]; 104 | const baseType = getBaseType(fieldObj.type); 105 | let typeToUse = 106 | this.config.scalars[baseType.name]?.output || baseType.name; 107 | 108 | if (isEnumType(baseType)) { 109 | typeToUse = 110 | (this.config.namespacedImportName 111 | ? `${this.config.namespacedImportName}.` 112 | : "") + 113 | this.config.convertName(baseType.name, { 114 | useTypesPrefix: this.config.enumPrefix, 115 | useTypesSuffix: this.config.enumSuffix, 116 | }); 117 | } 118 | 119 | const name = this.config.formatNamedField( 120 | aliasedField.alias, 121 | fieldObj.type, 122 | aliasedField.isConditional, 123 | unsetTypes 124 | ); 125 | if (unsetTypes) { 126 | return { 127 | type: "never", 128 | name, 129 | }; 130 | } 131 | 132 | const wrappedType = this.config.wrapTypeWithModifiers( 133 | typeToUse, 134 | fieldObj.type 135 | ); 136 | 137 | return { 138 | name, 139 | type: wrappedType, 140 | }; 141 | }); 142 | } 143 | 144 | transformLinkFields( 145 | fields: LinkField[], 146 | unsetTypes?: boolean 147 | ): ProcessResult { 148 | if (fields.length === 0) { 149 | return []; 150 | } 151 | 152 | return fields.map((field) => ({ 153 | name: field.alias || field.name, 154 | type: unsetTypes ? "never" : field.selectionSet, 155 | })); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/types.ts: -------------------------------------------------------------------------------- 1 | import type { ASTNode, FragmentDefinitionNode, DirectiveNode } from "graphql"; 2 | import { ParsedMapper } from "./mappers"; 3 | 4 | /** 5 | * A map between the GraphQL directive name and the identifier that should be used 6 | */ 7 | export type DirectiveArgumentAndInputFieldMappings = { [name: string]: string }; 8 | 9 | /** 10 | * Parsed directives map - a mapping between GraphQL directive name and the parsed mapper object, 11 | * including all required information for generating code for that mapping. 12 | */ 13 | export type ParsedDirectiveArgumentAndInputFieldMappings = { 14 | [name: string]: ParsedMapper; 15 | }; 16 | 17 | /** 18 | * Scalars map or a string, a map between the GraphQL scalar name and the identifier that should be used 19 | */ 20 | export type ScalarsMap = 21 | | string 22 | | { [name: string]: string | { input: string; output: string } }; 23 | /** 24 | * A normalized map between GraphQL scalar name and the identifier name 25 | */ 26 | export type NormalizedScalarsMap = { 27 | [name: string]: { 28 | input: string; 29 | output: string; 30 | }; 31 | }; 32 | /** 33 | * Parsed scalars map - a mapping between GraphQL scalar name and the parsed mapper object, 34 | * including all required information for generting code for that mapping. 35 | */ 36 | export type ParsedScalarsMap = { 37 | [name: string]: { 38 | input: ParsedMapper; 39 | output: ParsedMapper; 40 | }; 41 | }; 42 | /** 43 | * A raw configuration for enumValues map - can be represented with a single string value for a file path, 44 | * a map between enum name and a file path, or a map between enum name and an object with explicit enum values. 45 | */ 46 | export type EnumValuesMap = 47 | | string 48 | | { 49 | [enumName: string]: 50 | | string 51 | | ({ [key: string]: string | number } & AdditionalProps); 52 | }; 53 | export type ParsedEnumValuesMap = { 54 | [enumName: string]: { 55 | // If values are explictly set, this will include the mapped values 56 | mappedValues?: { [valueName: string]: string | number }; 57 | // The GraphQL enum name 58 | typeIdentifier: string; 59 | // The actual identifier that you should use in the code (original or aliased) 60 | sourceIdentifier?: string; 61 | // In case of external enum, this will contain the source file path 62 | sourceFile?: string; 63 | // If the identifier is external (imported) - this will contain the imported expression (including alias), otherwise null 64 | importIdentifier?: string; 65 | // Is defualt import is used to import the enum 66 | isDefault?: boolean; 67 | }; 68 | }; 69 | export type ConvertNameFn = ConvertFn; 70 | export type GetFragmentSuffixFn = ( 71 | node: FragmentDefinitionNode | string 72 | ) => string; 73 | 74 | export interface ConvertOptions { 75 | prefix?: string; 76 | suffix?: string; 77 | transformUnderscore?: boolean; 78 | } 79 | 80 | export type ConvertFn = ( 81 | node: ASTNode | string, 82 | options?: ConvertOptions & T 83 | ) => string; 84 | export type NamingConvention = "keep"; 85 | 86 | export type LoadedFragment = { 87 | name: string; 88 | onType: string; 89 | node: FragmentDefinitionNode; 90 | isExternal: boolean; 91 | importFrom?: string | null; 92 | } & AdditionalFields; 93 | 94 | export type DeclarationKind = "type" | "interface" | "class" | "abstract class"; 95 | 96 | export interface DeclarationKindConfig { 97 | directive?: DeclarationKind; 98 | scalar?: DeclarationKind; 99 | input?: DeclarationKind; 100 | type?: DeclarationKind; 101 | interface?: DeclarationKind; 102 | arguments?: DeclarationKind; 103 | } 104 | 105 | export interface AvoidOptionalsConfig { 106 | field?: boolean; 107 | object?: boolean; 108 | inputValue?: boolean; 109 | defaultValue?: boolean; 110 | resolvers?: boolean; 111 | } 112 | 113 | export interface ParsedImport { 114 | moduleName: string | null; 115 | propName: string; 116 | } 117 | 118 | export type FragmentDirectives = { 119 | fragmentDirectives?: Array; 120 | }; 121 | 122 | export interface ResolversNonOptionalTypenameConfig { 123 | unionMember?: boolean; 124 | interfaceImplementingType?: boolean; 125 | excludeTypes?: string[]; 126 | } 127 | -------------------------------------------------------------------------------- /packages/compiler/src/vendor/visitor-plugin-common/variables-to-object.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DirectiveNode, 3 | NameNode, 4 | TypeNode, 5 | ValueNode, 6 | VariableNode, 7 | } from "graphql/language/ast"; 8 | import { Kind } from "graphql/language/kinds"; 9 | import { BaseVisitorConvertOptions } from "./base-visitor"; 10 | import { 11 | ConvertNameFn, 12 | NormalizedScalarsMap, 13 | ParsedDirectiveArgumentAndInputFieldMappings, 14 | ParsedEnumValuesMap, 15 | } from "./types"; 16 | import { getBaseTypeNode, indent } from "./utils"; 17 | import { autoBind } from "../auto-bind"; 18 | 19 | export interface InterfaceOrVariable { 20 | name?: NameNode; 21 | variable?: VariableNode; 22 | type: TypeNode; 23 | defaultValue?: ValueNode; 24 | directives?: ReadonlyArray; 25 | } 26 | 27 | export class OperationVariablesToObject { 28 | constructor( 29 | protected _scalars: NormalizedScalarsMap, 30 | protected _convertName: ConvertNameFn, 31 | protected _namespacedImportName: string | null = null, 32 | protected _enumNames: string[] = [], 33 | protected _enumPrefix = true, 34 | protected _enumSuffix = true, 35 | protected _enumValues: ParsedEnumValuesMap = {}, 36 | protected _applyCoercion: Boolean = false, 37 | protected _directiveArgumentAndInputFieldMappings: ParsedDirectiveArgumentAndInputFieldMappings = {} 38 | ) { 39 | autoBind(this); 40 | } 41 | 42 | getName( 43 | node: TDefinitionType 44 | ): string | null { 45 | if (node.name) { 46 | if (typeof node.name === "string") { 47 | return node.name; 48 | } 49 | 50 | return node.name.value; 51 | } 52 | if (node.variable) { 53 | return node.variable.name.value; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | transform( 60 | variablesNode: ReadonlyArray 61 | ): string | null { 62 | if (!variablesNode || variablesNode.length === 0) { 63 | return null; 64 | } 65 | 66 | return ( 67 | variablesNode 68 | .map((variable) => indent(this.transformVariable(variable))) 69 | .join(`${this.getPunctuation()}\n`) + this.getPunctuation() 70 | ); 71 | } 72 | 73 | protected getScalar(name: string): string { 74 | const prefix = this._namespacedImportName 75 | ? `${this._namespacedImportName}.` 76 | : ""; 77 | 78 | return `${prefix}Scalars['${name}']`; 79 | } 80 | 81 | protected getDirectiveMapping(name: string): string { 82 | return `DirectiveArgumentAndInputFieldMappings['${name}']`; 83 | } 84 | 85 | protected getDirectiveOverrideType( 86 | directives: ReadonlyArray 87 | ): string | null { 88 | if (!this._directiveArgumentAndInputFieldMappings) return null; 89 | 90 | const type = directives 91 | .map((directive) => { 92 | const directiveName = directive.name.value; 93 | if (this._directiveArgumentAndInputFieldMappings[directiveName]) { 94 | return this.getDirectiveMapping(directiveName); 95 | } 96 | return null; 97 | }) 98 | .reverse() 99 | .find((a) => !!a); 100 | 101 | return type || null; 102 | } 103 | 104 | protected transformVariable( 105 | variable: TDefinitionType 106 | ): string { 107 | let typeValue: string; 108 | const prefix = this._namespacedImportName 109 | ? `${this._namespacedImportName}.` 110 | : ""; 111 | 112 | if (typeof variable.type === "string") { 113 | typeValue = variable.type; 114 | } else { 115 | const baseType = getBaseTypeNode(variable.type); 116 | const overrideType = variable.directives 117 | ? this.getDirectiveOverrideType(variable.directives) 118 | : null; 119 | const typeName = baseType.name.value; 120 | 121 | if (overrideType) { 122 | typeValue = overrideType; 123 | } else if (this._scalars[typeName]) { 124 | typeValue = this.getScalar(typeName); 125 | } else if (this._enumValues[typeName]?.sourceFile) { 126 | typeValue = 127 | this._enumValues[typeName].typeIdentifier || 128 | this._enumValues[typeName].sourceIdentifier!; 129 | } else { 130 | typeValue = `${prefix}${this._convertName(baseType, { 131 | useTypesPrefix: this._enumNames.includes(typeName) 132 | ? this._enumPrefix 133 | : true, 134 | useTypesSuffix: this._enumNames.includes(typeName) 135 | ? this._enumSuffix 136 | : true, 137 | })}`; 138 | } 139 | } 140 | 141 | const fieldName = this.getName(variable)!; 142 | const fieldType = this.wrapAstTypeWithModifiers( 143 | typeValue, 144 | variable.type, 145 | this._applyCoercion 146 | ); 147 | 148 | const hasDefaultValue = 149 | variable.defaultValue != null && 150 | typeof variable.defaultValue !== "undefined"; 151 | const isNonNullType = variable.type.kind === Kind.NON_NULL_TYPE; 152 | 153 | const formattedFieldString = this.formatFieldString( 154 | fieldName, 155 | isNonNullType, 156 | hasDefaultValue 157 | ); 158 | const formattedTypeString = this.formatTypeString( 159 | fieldType, 160 | isNonNullType, 161 | hasDefaultValue 162 | ); 163 | 164 | return `${formattedFieldString}: ${formattedTypeString}`; 165 | } 166 | 167 | public wrapAstTypeWithModifiers( 168 | _baseType: string, 169 | _typeNode: TypeNode, 170 | _applyCoercion?: Boolean 171 | ): string { 172 | throw new Error( 173 | `You must override "wrapAstTypeWithModifiers" of OperationVariablesToObject!` 174 | ); 175 | } 176 | 177 | protected formatFieldString( 178 | fieldName: string, 179 | _isNonNullType: boolean, 180 | _hasDefaultValue: boolean 181 | ): string { 182 | return fieldName; 183 | } 184 | 185 | protected formatTypeString( 186 | fieldType: string, 187 | isNonNullType: boolean, 188 | hasDefaultValue: boolean 189 | ): string { 190 | const prefix = this._namespacedImportName 191 | ? `${this._namespacedImportName}.` 192 | : ""; 193 | 194 | if (hasDefaultValue) { 195 | return `${prefix}Maybe<${fieldType}>`; 196 | } 197 | 198 | return fieldType; 199 | } 200 | 201 | protected getPunctuation(): string { 202 | return ","; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /packages/compiler/src/watch.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | import { getRawConfig, parseSchema } from "@ts-gql/config"; 3 | import { createWatcher } from "./watcher"; 4 | import { getGeneratedTypes } from "./get-generated-types"; 5 | import { applyFsOperation } from "./fs-operations"; 6 | import { lazyRequire } from "lazy-require.macro"; 7 | 8 | // TODO: handle changes incrementally 9 | export const watch = async (cwd: string) => { 10 | // we're requiring these here because we lazily require them in the code that generates the types 11 | // which is what we want so builds that don't have to regenerate files are fast 12 | // but for watch, a slightly slower start up is better than a slightly slower first build which requires type generation 13 | require("@babel/code-frame"); 14 | require("graphql/validation"); 15 | 16 | // not gonna respond to changes in the config because that would be a big peformance cost for practically no gain 17 | let rawConfig = await getRawConfig(cwd); 18 | 19 | // we want to lazily require this so it doesn't add cost to doing a regular build 20 | const chokidar = lazyRequire(); 21 | 22 | let getNext = createWatcher( 23 | chokidar.watch(["**/*.{ts,tsx}", rawConfig.schemaFilename], { 24 | cwd: rawConfig.directory, 25 | ignored: ["**/node_modules/**"], 26 | }) 27 | ); 28 | 29 | let lastPrintedErrors: string[] | undefined; 30 | let shouldPrintStartMessage = true; 31 | while (true) { 32 | await getNext(); 33 | 34 | if (shouldPrintStartMessage) { 35 | console.log("Started ts-gql watch"); 36 | console.log( 37 | "══════════════════════════════════════════════════════════════════════════" 38 | ); 39 | shouldPrintStartMessage = false; 40 | } 41 | 42 | let config = { 43 | ...rawConfig, 44 | ...parseSchema( 45 | rawConfig.schemaFilename, 46 | await fs.readFile(rawConfig.schemaFilename, "utf8") 47 | ), 48 | }; 49 | // we want to eagerly parse the schema so that the first change that someone makes 50 | // to a fragment/operation happens quickly 51 | // we're ignoring errors here though because they're handled in getGeneratedTypes 52 | try { 53 | config.schema(); 54 | } catch (err) {} 55 | let { fsOperations, errors } = await getGeneratedTypes(config, false); 56 | await Promise.all(fsOperations.map(applyFsOperation)); 57 | let willPrintErrors = 58 | errors.length && 59 | (lastPrintedErrors === undefined || 60 | lastPrintedErrors.length !== errors.length || 61 | errors.some((x, i) => lastPrintedErrors![i] !== x)); 62 | if (fsOperations.length) { 63 | console.error( 64 | `Updated ${fsOperations.length} file${ 65 | fsOperations.length === 1 ? "" : "s" 66 | }` 67 | ); 68 | } 69 | 70 | if (willPrintErrors) { 71 | for (let error of errors) { 72 | console.error(error); 73 | } 74 | } 75 | lastPrintedErrors = errors; 76 | if (willPrintErrors || fsOperations.length) 77 | console.log( 78 | "══════════════════════════════════════════════════════════════════════════" 79 | ); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /packages/compiler/src/watcher.ts: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/petehunt/bee47e20701329792153453409b1922b 2 | import type { FSWatcher } from "chokidar"; 3 | import assert from "node:assert"; 4 | 5 | interface WatcherFsEvent { 6 | type: "add" | "unlink" | "change"; 7 | path: string; 8 | } 9 | 10 | type WatcherEvent = WatcherFsEvent | { type: "ready" }; 11 | 12 | function createPromiseSignal() { 13 | let resolve: undefined | ((value: T) => void); 14 | 15 | let promise = new Promise((r: (value: T) => void) => { 16 | resolve = r; 17 | }); 18 | let resolved = false; 19 | 20 | return { 21 | promise, 22 | resolve(value: T) { 23 | assert(!resolved, "already resolved"); 24 | resolved = true; 25 | resolve!(value); 26 | }, 27 | }; 28 | } 29 | 30 | export const createWatcher = (watcher: FSWatcher) => { 31 | let started = false; 32 | let ready = false; 33 | let eventQueue: WatcherEvent[] = []; 34 | let signal = createPromiseSignal(); 35 | let lastError: Error | null = null; 36 | function pushEvent(event: WatcherEvent) { 37 | eventQueue.push(event); 38 | if (eventQueue.length === 1) { 39 | signal.resolve(); 40 | } 41 | } 42 | async function start() { 43 | assert(!started, "already started"); 44 | started = true; 45 | 46 | ready = false; 47 | 48 | watcher.on("ready", () => { 49 | ready = true; 50 | pushEvent({ type: "ready" }); 51 | }); 52 | 53 | watcher.on("add", (path) => { 54 | if (!ready) { 55 | return; 56 | } 57 | 58 | pushEvent({ type: "add", path }); 59 | }); 60 | 61 | watcher.on("change", async (path) => { 62 | pushEvent({ type: "change", path }); 63 | }); 64 | 65 | watcher.on("unlink", (path) => { 66 | pushEvent({ type: "unlink", path }); 67 | }); 68 | 69 | watcher.on("error", (err) => { 70 | lastError = err; 71 | }); 72 | } 73 | return async () => { 74 | if (lastError) { 75 | let err = lastError; 76 | lastError = null; 77 | throw err; 78 | } 79 | 80 | if (!started) { 81 | await start(); 82 | } 83 | 84 | if (eventQueue.length === 0) { 85 | await signal.promise; 86 | } 87 | 88 | const currentEventQueue = eventQueue; 89 | eventQueue = []; 90 | signal = createPromiseSignal(); 91 | 92 | return currentEventQueue; 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /packages/compiler/src/weakMemoize.ts: -------------------------------------------------------------------------------- 1 | export function weakMemoize( 2 | fn: (arg: Arg) => Return 3 | ): (arg: Arg) => Return { 4 | const cache = new WeakMap(); 5 | return (arg: Arg) => { 6 | if (cache.has(arg)) return cache.get(arg)!; 7 | const result = fn(arg); 8 | cache.set(arg, result); 9 | return result; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/config 2 | 3 | ## 0.9.2 4 | 5 | ### Patch Changes 6 | 7 | - [`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad) Thanks [@emmatown](https://github.com/emmatown)! - Add `exports` field to `package.json` 8 | 9 | ## 0.9.1 10 | 11 | ### Patch Changes 12 | 13 | - [`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@16` 14 | 15 | ## 0.9.0 16 | 17 | ### Minor Changes 18 | 19 | - [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Added support for `"mode": "no-transform"` and `"mode": "mixed"`. See https://github.com/Thinkmill/ts-gql/blob/main/docs/no-transform.md for more details 20 | 21 | ## 0.8.0 22 | 23 | ### Minor Changes 24 | 25 | - [`603c9ed`](https://github.com/Thinkmill/ts-gql/commit/603c9ed186377c8de4517a8371aec08b45a3a425) Thanks [@emmatown](https://github.com/emmatown)! - Add `schemaFilename` to `Config`, rename `schema` to `schemaFilename` on `RawConfig` and throw more useful errors when validating schemas 26 | 27 | ### Patch Changes 28 | 29 | - [`3149ffe`](https://github.com/Thinkmill/ts-gql/commit/3149ffe2ffb428273e80451d8a67873073e052c8) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@^15.0.0` 30 | 31 | ## 0.7.1 32 | 33 | ### Patch Changes 34 | 35 | - [`503d9c3`](https://github.com/Thinkmill/ts-gql/commit/503d9c361f32cb4855d017b7340c3b6db45d181b) Thanks [@emmatown](https://github.com/emmatown)! - Optimise lazy requires 36 | 37 | ## 0.7.0 38 | 39 | ### Minor Changes 40 | 41 | - [`71f257e`](https://github.com/Thinkmill/ts-gql/commit/71f257e5ec9152b01bcb86aa06810a8d84e1441d) Thanks [@emmatown](https://github.com/emmatown)! - Change `schema` property of `Config` from having the `GraphQLSchema` to a function that returns the `GraphQLSchema`. This is so that the schema is not unnecessarily parsed when it doesn't need to be 42 | 43 | ### Patch Changes 44 | 45 | - [`4e88d55`](https://github.com/Thinkmill/ts-gql/commit/4e88d551463c108fe30a609c24fa641e8f9ec88b) Thanks [@emmatown](https://github.com/emmatown)! - Only import necessary modules from `graphql` 46 | 47 | ## 0.6.0 48 | 49 | ### Minor Changes 50 | 51 | - [`d6d5594`](https://github.com/Thinkmill/ts-gql/commit/d6d55946c9dfc118d87ba34b79d48d48a3144e4d) Thanks [@emmatown](https://github.com/emmatown)! - Replace `readSchema` and `readSchemaSync` with `parseSchema` and `hashSchema` 52 | 53 | * [`7f10732`](https://github.com/Thinkmill/ts-gql/commit/7f10732c53b1b9541414b6c343ad7cd1e35e122c) Thanks [@emmatown](https://github.com/emmatown)! - Cache schema parsing, remove `hashSchema`, make `parseSchema` return `schemaHash` and `schema` 54 | 55 | - [`90d1567`](https://github.com/Thinkmill/ts-gql/commit/90d15672f4737d8a1c15429f680790c9abdccf58) Thanks [@emmatown](https://github.com/emmatown)! - Add `schemaHash` to `Config` 56 | 57 | ## 0.5.0 58 | 59 | ### Minor Changes 60 | 61 | - [`fba7341`](https://github.com/Thinkmill/ts-gql/commit/fba7341a1418e0a9d555172dc5c6e86899fa6ed3) Thanks [@emmatown](https://github.com/emmatown)! - Add `readonlyTypes` option 62 | 63 | ## 0.4.0 64 | 65 | ### Minor Changes 66 | 67 | - [`4be8faa`](https://github.com/Thinkmill/ts-gql/commit/4be8faafa0fba17efa491a0aec8ddbb472aa5572) Thanks [@emmatown](https://github.com/emmatown)! - Replace `nonOptionalTypename` option with `addTypename` option 68 | 69 | ## 0.3.0 70 | 71 | ### Minor Changes 72 | 73 | - [`d798897`](https://github.com/Thinkmill/ts-gql/commit/d7988972e801c41bb96aaa4dec5763ebae73e30e) Thanks [@emmatown](https://github.com/emmatown)! - Add nonOptionalTypename option 74 | 75 | ## 0.2.0 76 | 77 | ### Minor Changes 78 | 79 | - [`caa1974`](https://github.com/Thinkmill/ts-gql/commit/caa19743de1aa1345795691b8d4eea58c052fc8f) Thanks [@emmatown](https://github.com/emmatown)! - Support scalars option 80 | 81 | ## 0.1.0 82 | 83 | ### Minor Changes 84 | 85 | - [`e42383b`](https://github.com/Thinkmill/ts-gql/commit/e42383b5970a554462384f9851aabc173f7fcf52) Thanks [@emmatown](https://github.com/emmatown)! - Initial Release 86 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/config", 3 | "version": "0.9.2", 4 | "main": "dist/config.cjs.js", 5 | "module": "dist/config.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/config.esm.js", 9 | "default": "./dist/config.cjs.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@babel/runtime": "^7.9.2", 16 | "find-pkg-json-field-up": "^1.0.1", 17 | "lazy-require.macro": "^0.1.0", 18 | "superstruct": "^0.10.12" 19 | }, 20 | "peerDependencies": { 21 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16" 22 | }, 23 | "devDependencies": { 24 | "graphql": "^16.10.0" 25 | }, 26 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/config" 27 | } 28 | -------------------------------------------------------------------------------- /packages/config/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { GraphQLSchema } from "graphql/type/schema"; 3 | import { 4 | findPkgJsonFieldUp, 5 | findPkgJsonFieldUpSync, 6 | } from "find-pkg-json-field-up"; 7 | import fs from "fs"; 8 | import { promisify } from "util"; 9 | import { parseSchema, BatchGraphQLError } from "./parse-schema"; 10 | 11 | export { parseSchema, BatchGraphQLError }; 12 | 13 | export class ConfigNotFoundError extends Error {} 14 | 15 | export type Config = { 16 | schema: () => GraphQLSchema; 17 | schemaHash: string; 18 | } & RawConfig; 19 | 20 | export type RawConfig = { 21 | directory: string; 22 | schemaFilename: string; 23 | scalars: Record; 24 | addTypename: boolean; 25 | readonlyTypes: boolean; 26 | mode: "transform" | "no-transform" | "mixed"; 27 | }; 28 | 29 | function parseFieldToConfig({ 30 | packageJson, 31 | directory, 32 | }: { 33 | packageJson: Record; 34 | directory: string; 35 | }): RawConfig { 36 | let field = packageJson["ts-gql"]; 37 | if ( 38 | typeof field === "object" && 39 | field !== null && 40 | typeof field.schema === "string" 41 | ) { 42 | return { 43 | schemaFilename: path.resolve(directory, field.schema), 44 | directory, 45 | scalars: field.scalars || {}, 46 | addTypename: field.addTypename ?? true, 47 | readonlyTypes: field.readonlyTypes ?? true, 48 | mode: field.mode ?? "transform", 49 | }; 50 | } 51 | throw new ConfigNotFoundError("ts-gql config not found"); 52 | } 53 | 54 | export async function getRawConfig(cwd: string) { 55 | return parseFieldToConfig(await findPkgJsonFieldUp("ts-gql", cwd)); 56 | } 57 | 58 | export function getRawConfigSync(cwd: string) { 59 | return parseFieldToConfig(findPkgJsonFieldUpSync("ts-gql", cwd)); 60 | } 61 | 62 | const readFile = promisify(fs.readFile); 63 | 64 | export async function getConfig(cwd: string): Promise { 65 | let config = await getRawConfig(cwd); 66 | let schemaContents = await readFile(config.schemaFilename, "utf8"); 67 | return { 68 | ...config, 69 | ...parseSchema(config.schemaFilename, schemaContents), 70 | }; 71 | } 72 | 73 | export function getConfigSync(cwd: string): Config { 74 | let config = getRawConfigSync(cwd); 75 | let schemaContents = fs.readFileSync(config.schemaFilename, "utf8"); 76 | return { 77 | ...config, 78 | ...parseSchema(config.schemaFilename, schemaContents), 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /packages/config/src/parse-schema.ts: -------------------------------------------------------------------------------- 1 | import { version } from "graphql/version"; 2 | import type { GraphQLSchema } from "graphql/type/schema"; 3 | import { lazyRequire } from "lazy-require.macro"; 4 | import { parse } from "graphql/language/parser"; 5 | import { GraphQLError } from "graphql/error/GraphQLError"; 6 | import crypto from "crypto"; 7 | 8 | function hashSchema(input: string) { 9 | let md5sum = crypto.createHash("md5"); 10 | 11 | md5sum.update(version); 12 | md5sum.update("v2"); 13 | md5sum.update(input); 14 | return md5sum.digest("hex"); 15 | } 16 | 17 | let schemaCache: Record< 18 | string, 19 | { schemaHash: string; schema: () => GraphQLSchema } 20 | > = {}; 21 | 22 | export function parseSchema(filename: string, content: string) { 23 | let schemaHash = hashSchema(content); 24 | if (schemaCache[filename]?.schemaHash !== schemaHash) { 25 | let schema: GraphQLSchema; 26 | schemaCache[filename] = { 27 | schemaHash, 28 | schema: () => { 29 | if (!schema) { 30 | schema = uncachedParseSchema(filename, content); 31 | } 32 | return schema; 33 | }, 34 | }; 35 | } 36 | return schemaCache[filename]; 37 | } 38 | 39 | export class BatchGraphQLError extends Error { 40 | errors: readonly GraphQLError[]; 41 | constructor(errors: readonly GraphQLError[]) { 42 | super( 43 | "There are validation errors in your GraphQL schema. If you're seeing this, there's likely a bug in ts-gql's error printing." 44 | ); 45 | this.errors = errors; 46 | } 47 | } 48 | 49 | function uncachedParseSchema(filename: string, content: string) { 50 | if (!filename.endsWith(".json")) { 51 | const ast = parse(content); 52 | const { validateSDL } = 53 | lazyRequire(); 54 | const validationErrors = validateSDL(ast); 55 | if (validationErrors.length) { 56 | throw new BatchGraphQLError(validationErrors); 57 | } 58 | const { buildASTSchema } = 59 | lazyRequire(); 60 | 61 | return buildASTSchema(ast, { assumeValidSDL: true }); 62 | } 63 | const { buildClientSchema } = 64 | lazyRequire(); 65 | 66 | let schema = JSON.parse(content); 67 | const unpackedSchemaJson = schema.data ? schema.data : schema; 68 | if (!unpackedSchemaJson.__schema) { 69 | throw new Error( 70 | `${filename} should contain a valid introspection query result` 71 | ); 72 | } 73 | return buildClientSchema(unpackedSchemaJson); 74 | } 75 | -------------------------------------------------------------------------------- /packages/eslint-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/eslint-plugin 2 | 3 | ## 0.9.1 4 | 5 | ### Patch Changes 6 | 7 | - [`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad) Thanks [@emmatown](https://github.com/emmatown)! - Add `exports` field to `package.json` 8 | 9 | - Updated dependencies [[`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad)]: 10 | - @ts-gql/config@0.9.2 11 | 12 | ## 0.9.0 13 | 14 | ### Minor Changes 15 | 16 | - [#117](https://github.com/Thinkmill/ts-gql/pull/117) [`9cc5227`](https://github.com/Thinkmill/ts-gql/commit/9cc52277322af7e9cc3007abc9125e4af210e9ca) Thanks [@emmatown](https://github.com/emmatown)! - Support `typescript-eslint@6` 17 | 18 | ## 0.8.5 19 | 20 | ### Patch Changes 21 | 22 | - [`f5e7421`](https://github.com/Thinkmill/ts-gql/commit/f5e7421f3525fd34b64e6c1dfef751ee09e52625) Thanks [@emmatown](https://github.com/emmatown)! - Fix linting files with array destructuring that ignore elements like `[,something]` 23 | 24 | ## 0.8.4 25 | 26 | ### Patch Changes 27 | 28 | - [#100](https://github.com/Thinkmill/ts-gql/pull/100) [`63d269d`](https://github.com/Thinkmill/ts-gql/commit/63d269d441420dca5c4c934ec1c60b35831286a2) Thanks [@emmatown](https://github.com/emmatown)! - Fixed incorrect fix/error for casts on `gql` calls in source files in the same directory as the config. 29 | 30 | ## 0.8.3 31 | 32 | ### Patch Changes 33 | 34 | - [#96](https://github.com/Thinkmill/ts-gql/pull/96) [`1cecb2c`](https://github.com/Thinkmill/ts-gql/commit/1cecb2cd0c4c38f6da6d8f4914da5f81bef741f5) Thanks [@emmatown](https://github.com/emmatown)! - Fixed an internal error occurring when writing a selection set on a union type wrapped in a non-null or list type. 35 | 36 | ## 0.8.2 37 | 38 | ### Patch Changes 39 | 40 | - [`5b800e7`](https://github.com/Thinkmill/ts-gql/commit/5b800e763cb428c972ff0bfb85592405bb513754) Thanks [@emmatown](https://github.com/emmatown)! - Republish after broken release 41 | 42 | ## 0.8.1 43 | 44 | ### Patch Changes 45 | 46 | - [`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@16` 47 | 48 | - Updated dependencies [[`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd)]: 49 | - @ts-gql/config@0.9.1 50 | 51 | ## 0.8.0 52 | 53 | ### Minor Changes 54 | 55 | - [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Added support for `"mode": "no-transform"` and `"mode": "mixed"`. See https://github.com/Thinkmill/ts-gql/blob/main/docs/no-transform.md for more details 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies [[`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d)]: 60 | - @ts-gql/config@0.9.0 61 | 62 | ## 0.7.2 63 | 64 | ### Patch Changes 65 | 66 | - [`3149ffe`](https://github.com/Thinkmill/ts-gql/commit/3149ffe2ffb428273e80451d8a67873073e052c8) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@^15.0.0` 67 | 68 | - Updated dependencies [[`603c9ed`](https://github.com/Thinkmill/ts-gql/commit/603c9ed186377c8de4517a8371aec08b45a3a425), [`3149ffe`](https://github.com/Thinkmill/ts-gql/commit/3149ffe2ffb428273e80451d8a67873073e052c8)]: 69 | - @ts-gql/config@0.8.0 70 | 71 | ## 0.7.1 72 | 73 | ### Patch Changes 74 | 75 | - Updated dependencies [[`4e88d55`](https://github.com/Thinkmill/ts-gql/commit/4e88d551463c108fe30a609c24fa641e8f9ec88b), [`71f257e`](https://github.com/Thinkmill/ts-gql/commit/71f257e5ec9152b01bcb86aa06810a8d84e1441d)]: 76 | - @ts-gql/config@0.7.0 77 | 78 | ## 0.7.0 79 | 80 | ### Minor Changes 81 | 82 | - [`d4a220a`](https://github.com/Thinkmill/ts-gql/commit/d4a220ad74a7e57bafcd2c3ec3b22cafabbfe744) Thanks [@emmatown](https://github.com/emmatown)! - Add autofix for undefined variables 83 | 84 | ### Patch Changes 85 | 86 | - Updated dependencies [[`d6d5594`](https://github.com/Thinkmill/ts-gql/commit/d6d55946c9dfc118d87ba34b79d48d48a3144e4d), [`7f10732`](https://github.com/Thinkmill/ts-gql/commit/7f10732c53b1b9541414b6c343ad7cd1e35e122c), [`90d1567`](https://github.com/Thinkmill/ts-gql/commit/90d15672f4737d8a1c15429f680790c9abdccf58)]: 87 | - @ts-gql/config@0.6.0 88 | 89 | ## 0.6.1 90 | 91 | ### Patch Changes 92 | 93 | - [`94d642d`](https://github.com/Thinkmill/ts-gql/commit/94d642d514dac32c183881cfe75e6cc61851707d) Thanks [@emmatown](https://github.com/emmatown)! - Improve heuristic to determine when it is necessary to fetch an id 94 | 95 | ## 0.6.0 96 | 97 | ### Minor Changes 98 | 99 | - [`63bbe54`](https://github.com/Thinkmill/ts-gql/commit/63bbe543b2ba34e14565ca3627187e37a9bbd619) Thanks [@emmatown](https://github.com/emmatown)! - Throw a fixable error when ids aren't fetched for types that have them 100 | 101 | ## 0.5.0 102 | 103 | ### Minor Changes 104 | 105 | - [`4f18b26`](https://github.com/Thinkmill/ts-gql/commit/4f18b264c0b3f6cb754b327b70ef47894f387492) Thanks [@emmatown](https://github.com/emmatown)! - Determine whether a given gql tag is a ts-gql gql tag by the import rather than the TypeScript type to improve the ESLint plugin's performance and remove the dependency on the TypeScript compiler 106 | 107 | ## 0.4.5 108 | 109 | ### Patch Changes 110 | 111 | - Updated dependencies [[`fba7341`](https://github.com/Thinkmill/ts-gql/commit/fba7341a1418e0a9d555172dc5c6e86899fa6ed3)]: 112 | - @ts-gql/config@0.5.0 113 | 114 | ## 0.4.4 115 | 116 | ### Patch Changes 117 | 118 | - Updated dependencies [[`4be8faa`](https://github.com/Thinkmill/ts-gql/commit/4be8faafa0fba17efa491a0aec8ddbb472aa5572)]: 119 | - @ts-gql/config@0.4.0 120 | 121 | ## 0.4.3 122 | 123 | ### Patch Changes 124 | 125 | - Updated dependencies [[`d798897`](https://github.com/Thinkmill/ts-gql/commit/d7988972e801c41bb96aaa4dec5763ebae73e30e)]: 126 | - @ts-gql/config@0.3.0 127 | 128 | ## 0.4.2 129 | 130 | ### Patch Changes 131 | 132 | - Updated dependencies [[`caa1974`](https://github.com/Thinkmill/ts-gql/commit/caa19743de1aa1345795691b8d4eea58c052fc8f)]: 133 | - @ts-gql/config@0.2.0 134 | 135 | ## 0.4.1 136 | 137 | ### Patch Changes 138 | 139 | - [`e42383b`](https://github.com/Thinkmill/ts-gql/commit/e42383b5970a554462384f9851aabc173f7fcf52) Thanks [@emmatown](https://github.com/emmatown)! - Use `@ts-gql/config` package 140 | 141 | - Updated dependencies [[`e42383b`](https://github.com/Thinkmill/ts-gql/commit/e42383b5970a554462384f9851aabc173f7fcf52)]: 142 | - @ts-gql/config@0.1.0 143 | 144 | ## 0.4.0 145 | 146 | ### Minor Changes 147 | 148 | - [`e4c60ad`](https://github.com/Thinkmill/ts-gql/commit/e4c60adcc45abba018c4b9d4d0379e7d529a9af1) Thanks [@emmatown](https://github.com/emmatown)! - Use new technique to generate types. 149 | 150 | This requires you to use `@ts-gql/compiler` and `@ts-gql/babel-plugin` in addition to `@ts-gql/eslint-plugin`. 151 | 152 | Configuration also now lives in the `package.json` like this: 153 | 154 | ```json 155 | { 156 | "ts-gql": { 157 | "schema": "schema.graphql" 158 | } 159 | } 160 | ``` 161 | 162 | ## 0.3.1 163 | 164 | ### Patch Changes 165 | 166 | - [`756f672`](https://github.com/Thinkmill/ts-gql/commit/756f67221ce5bf44a7a949779df8413712eed7ab) Thanks [@emmatown](https://github.com/emmatown)! - Improve fragment type not found error 167 | 168 | ## 0.3.0 169 | 170 | ### Minor Changes 171 | 172 | - [`8175079`](https://github.com/Thinkmill/ts-gql/commit/817507911de80cb628e01f42d1c547915f811415) Thanks [@emmatown](https://github.com/emmatown)! - Represent enums as types rather than TS enums and add scalars option 173 | 174 | ## 0.2.0 175 | 176 | ### Minor Changes 177 | 178 | - [`8485b1a`](https://github.com/Thinkmill/ts-gql/commit/8485b1a28228feea836d076cc7dd1a0691414248) Thanks [@emmatown](https://github.com/emmatown)! - Remove -with-required-variables types 179 | 180 | ## 0.1.0 181 | 182 | ### Minor Changes 183 | 184 | - [`b444283`](https://github.com/Thinkmill/ts-gql/commit/b44428353e6e94f7df60b8ffc409b44b6fbca1ca) Thanks [@emmatown](https://github.com/emmatown)! - Initial release 185 | -------------------------------------------------------------------------------- /packages/eslint-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/eslint-plugin", 3 | "version": "0.9.1", 4 | "main": "dist/eslint-plugin.cjs.js", 5 | "module": "dist/eslint-plugin.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/eslint-plugin.esm.js", 9 | "default": "./dist/eslint-plugin.cjs.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "license": "MIT", 14 | "files": [ 15 | "dist" 16 | ], 17 | "dependencies": { 18 | "@babel/runtime": "^7.9.2", 19 | "@ts-gql/config": "^0.9.2", 20 | "@typescript-eslint/utils": "^6.3.0", 21 | "find-pkg-json-field-up": "^1.0.1", 22 | "slash": "^3.0.0" 23 | }, 24 | "peerDependencies": { 25 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16" 26 | }, 27 | "devDependencies": { 28 | "@ts-gql/tag": "*", 29 | "@typescript-eslint/parser": "^6.3.0", 30 | "eslint-snapshot-test": "^3.1.2", 31 | "graphql": "^16.10.0", 32 | "tempy": "1.0.1" 33 | }, 34 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/eslint-plugin" 35 | } 36 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/allow-no-transform.mixed.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag/no-transform"; 2 | 3 | gql` 4 | query Thing { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/allow-transform.mixed.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/basic.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/disallow-interpolation-with-transform.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | const x: any = undefined; 4 | 5 | gql` 6 | query Thing { 7 | something 8 | } 9 | ${ 10 | // @ts-ignore 11 | x 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/disallow-no-transform.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag/no-transform"; 2 | 3 | gql` 4 | query Thing { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/disallow-transform.no-transform.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/incorrect-fragment-interpolate-num.no-transform.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag/no-transform"; 2 | 3 | const x: any = undefined; 4 | 5 | gql` 6 | query Thing { 7 | something 8 | ...Blah_x 9 | ...Other_x 10 | } 11 | ${x} 12 | ` as import(// @ts-ignore 13 | "../__generated__/ts-gql/Thing").type; 14 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/interface.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | node { 6 | id 7 | ... on SomethingNode { 8 | something 9 | } 10 | ... on AnotherNode { 11 | another 12 | } 13 | } 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/missing-id.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | someObj { 6 | other 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/missing-variable-with-another-variable-with-comma-after.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | // prettier-ignore 4 | gql` 5 | query Thing($other: String,,) { 6 | optional(thing: $thing) 7 | other: optional(thing: $other) 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/missing-variable-with-another-variable.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing($other: String) { 5 | optional(thing: $thing) 6 | other: optional(thing: $other) 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/missing-variable.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | optional(thing: $thing) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/multiple-validation-errors.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | a 6 | b 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/no-name.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/parse-error.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | que Thing { 5 | something 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/union.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | union { 6 | ... on A { 7 | a 8 | } 9 | ... on B { 10 | b 11 | } 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/unknown-variable-type.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing($thing: DoesNotExist) { 5 | optional(thing: $thing) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/unused-variable.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing($thing: String) { 5 | optional 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/with-array-destructuring-that-ignores.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing { 5 | optional 6 | } 7 | `; 8 | 9 | const [, b] = ["a", "b"]; 10 | 11 | console.log(b); 12 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/__fixtures__/wrong-variable-type.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag"; 2 | 3 | gql` 4 | query Thing($thing: ID) { 5 | optional(thing: $thing) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/get-nodes.ts: -------------------------------------------------------------------------------- 1 | // loosely based off https://github.com/discord/eslint-traverse/blob/master/index.js 2 | 3 | import { TSESLint, TSESTree } from "@typescript-eslint/utils"; 4 | 5 | export function* getNodes( 6 | context: TSESLint.RuleContext, 7 | node: TSESTree.Node 8 | ) { 9 | let allVisitorKeys = context.getSourceCode().visitorKeys; 10 | let queue = [node]; 11 | 12 | while (queue.length) { 13 | let currentNode = queue.shift()!; 14 | 15 | yield currentNode; 16 | 17 | let visitorKeys = allVisitorKeys[currentNode.type]; 18 | if (!visitorKeys) continue; 19 | 20 | for (let visitorKey of visitorKeys) { 21 | let child = (currentNode as any)[visitorKey] as 22 | | TSESTree.Node 23 | | TSESTree.Node[] 24 | | undefined; 25 | 26 | if (!child) { 27 | continue; 28 | } else if (Array.isArray(child)) { 29 | for (const node of child) { 30 | if (node) { 31 | queue.push(node); 32 | } 33 | } 34 | } else { 35 | queue.push(child); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/test.ts: -------------------------------------------------------------------------------- 1 | import { SnapshotCreator } from "eslint-snapshot-test"; 2 | import { rules } from "."; 3 | import { Config } from "@ts-gql/config"; 4 | import fs from "fs"; 5 | import { buildSchema } from "graphql"; 6 | import path from "path"; 7 | import { directory } from "tempy"; 8 | import { schema } from "../../compiler/src/test/test-schema"; 9 | 10 | let snapshotCreator = new SnapshotCreator({ 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | sourceType: "module", 18 | }, 19 | }); 20 | 21 | let builtSchema = buildSchema(schema); 22 | 23 | let testConfig: Config = { 24 | addTypename: true, 25 | directory: __dirname, 26 | readonlyTypes: true, 27 | scalars: {}, 28 | schemaHash: "123", 29 | schemaFilename: "schema.graphql", 30 | schema: () => builtSchema, 31 | mode: "transform", 32 | }; 33 | 34 | let fixturesPath = path.join(__dirname, "__fixtures__"); 35 | 36 | let fixtures = fs.readdirSync(fixturesPath); 37 | 38 | for (const fixture of fixtures) { 39 | test(fixture, async () => { 40 | const fixturePath = path.join(fixturesPath, fixture); 41 | const code = fs.readFileSync(fixturePath, "utf8"); 42 | let result = snapshotCreator 43 | .mark({ 44 | code, 45 | rule: rules["ts-gql"], 46 | ruleName: "ts-gql", 47 | }) 48 | .withFileName(fixturePath) 49 | .withOptions([ 50 | { 51 | ...testConfig, 52 | mode: fixture.endsWith(".no-transform.ts") 53 | ? "no-transform" 54 | : fixture.endsWith(".mixed.ts") 55 | ? "mixed" 56 | : "transform", 57 | }, 58 | ]) 59 | .render(); 60 | if (result.fixedOutput === code) { 61 | delete result.fixedOutput; 62 | } 63 | expect(result).toMatchSnapshot(); 64 | }); 65 | } 66 | 67 | test("config and file in the same directory", () => 68 | directory.task(async (tmpPath) => { 69 | const filePath = path.join(tmpPath, "test.ts"); 70 | fs.writeFileSync( 71 | `${tmpPath}/package.json`, 72 | JSON.stringify({ 73 | name: "blah", 74 | "ts-gql": { 75 | schema: "schema.graphql", 76 | }, 77 | }) 78 | ); 79 | fs.writeFileSync(`${tmpPath}/schema.graphql`, schema); 80 | 81 | const code = ` 82 | import { gql } from "@ts-gql/tag"; 83 | gql\` 84 | query Something { 85 | hello 86 | } 87 | \`; 88 | `; 89 | fs.writeFileSync(filePath, code); 90 | let result = snapshotCreator 91 | .mark({ 92 | code, 93 | rule: rules["ts-gql"], 94 | ruleName: "ts-gql", 95 | }) 96 | .withFileName(filePath) 97 | .render(); 98 | expect(result).toMatchInlineSnapshot(` 99 | { 100 | "fixedOutput": " 101 | import { gql } from "@ts-gql/tag"; 102 | gql\` 103 | query Something { 104 | hello 105 | } 106 | \`as import("./__generated__/ts-gql/Something").type; 107 | ", 108 | "lintMessages": [ 109 | { 110 | "column": 5, 111 | "endColumn": 6, 112 | "endLine": 7, 113 | "fix": { 114 | "range": [ 115 | 100, 116 | 100, 117 | ], 118 | "text": "as import("./__generated__/ts-gql/Something").type", 119 | }, 120 | "line": 3, 121 | "message": "You must cast gql tags with the generated type", 122 | "messageId": "mustUseAs", 123 | "nodeType": "TaggedTemplateExpression", 124 | "ruleId": "ts-gql", 125 | "severity": 2, 126 | }, 127 | ], 128 | "snapshot": " 129 | 130 | import { gql } from "@ts-gql/tag"; 131 | gql\` 132 | ~~~~ [You must cast gql tags with the generated type] 133 | query Something { 134 | ~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type] 135 | hello 136 | ~~~~~~~~~~~~~ [You must cast gql tags with the generated type] 137 | } 138 | ~~~~~~~ [You must cast gql tags with the generated type] 139 | \`; 140 | ~~~~~ [You must cast gql tags with the generated type] 141 | ", 142 | } 143 | `); 144 | })); 145 | -------------------------------------------------------------------------------- /packages/eslint-plugin/src/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | export function hashString(input: string) { 4 | let md5sum = crypto.createHash("md5"); 5 | md5sum.update(input); 6 | return md5sum.digest("hex"); 7 | } 8 | 9 | export function parseTsGqlMeta(content: string) { 10 | let result = /ts-gql-meta-begin([^]+)ts-gql-meta-end/m.exec(content); 11 | if (result === null) { 12 | throw new Error( 13 | "could not find ts-gql meta in the following contents:\n" + content 14 | ); 15 | } 16 | return JSON.parse(result[1]); 17 | } 18 | -------------------------------------------------------------------------------- /packages/fetch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/fetch 2 | 3 | ## 0.1.3 4 | 5 | ### Patch Changes 6 | 7 | - [`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad) Thanks [@emmatown](https://github.com/emmatown)! - Add `exports` field to `package.json` 8 | 9 | - Updated dependencies [[`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad)]: 10 | - @ts-gql/tag@0.7.1 11 | 12 | ## 0.1.2 13 | 14 | ### Patch Changes 15 | 16 | - Updated dependencies [[`bc8d6eb`](https://github.com/Thinkmill/ts-gql/commit/bc8d6ebbf1021829de24d3c916dad5e0b3ab1edf)]: 17 | - @ts-gql/tag@0.7.0 18 | 19 | ## 0.1.1 20 | 21 | ### Patch Changes 22 | 23 | - [`5b800e7`](https://github.com/Thinkmill/ts-gql/commit/5b800e763cb428c972ff0bfb85592405bb513754) Thanks [@emmatown](https://github.com/emmatown)! - Republish after broken release 24 | 25 | ## 0.1.0 26 | 27 | ### Minor Changes 28 | 29 | - [`c9356b8`](https://github.com/Thinkmill/ts-gql/commit/c9356b86d05c368409dced5f111d2bbcdf00f586) Thanks [@emmatown](https://github.com/emmatown)! - Initial release 30 | 31 | ### Patch Changes 32 | 33 | - Updated dependencies [[`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd)]: 34 | - @ts-gql/tag@0.6.1 35 | -------------------------------------------------------------------------------- /packages/fetch/README.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/fetch 2 | 3 | > A small wrapper over `fetch` to call GraphQL APIs with ts-gql's type-safety. 4 | 5 | ```tsx 6 | import { createFetcher, GraphQLErrorResult, Fetcher } from "@ts-gql/fetch"; 7 | import { gql } from "@ts-gql/tag" 8 | // or 9 | import { gql } from "@ts-gql/tag/no-transform" 10 | 11 | const fetchGraphQL = createFetcher("https://some-graphql-api"); 12 | 13 | const someQueryOrMutation = gql`...` as import(...).type; 14 | 15 | const result = await fetchGraphQL({ operation: someQueryOrMutation }); 16 | ``` 17 | 18 | If any GraphQL errors are returned, an instance of `GraphQLErrorResult` is thrown which has the `data` and `errors` on it. 19 | 20 | If you want to change how the fetching occurs but keep the types, you can re-use the `Fetcher` type and write your own implementation like this: 21 | 22 | ```ts 23 | import { GraphQLErrorResult, Fetcher } from "@ts-gql/fetch"; 24 | import { DocumentNode, print } from "graphql"; 25 | 26 | const fetchGraphQL: Fetcher = (( 27 | operation: DocumentNode, 28 | variables?: Record 29 | ) => { 30 | return fetch("https://some-graphql-api", { 31 | method: "POST", 32 | headers: { 33 | "Content-Type": "application/json", 34 | }, 35 | body: JSON.stringify({ 36 | query: print(operation), 37 | variables, 38 | }), 39 | }) 40 | .then((res) => res.json()) 41 | .then((data) => { 42 | if (data.errors?.length) { 43 | throw new GraphQLErrorResult(data.data, data.errors); 44 | } 45 | return data.data; 46 | }); 47 | }) as any; 48 | ``` 49 | 50 | When using `@ts-gql/fetch`, make sure to set `"addTypename": false` to your ts-gql config so that the generated types only include `__typename` in the returned fields if explicitly requested since `@ts-gql/fetch` won't implicitly add `__typename` like some other GraphQL clients. 51 | -------------------------------------------------------------------------------- /packages/fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/fetch", 3 | "version": "0.1.3", 4 | "main": "dist/fetch.cjs.js", 5 | "module": "dist/fetch.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/fetch.esm.js", 9 | "default": "./dist/fetch.cjs.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@ts-gql/tag": "^0.7.1" 16 | }, 17 | "peerDependencies": { 18 | "graphql": "15 || 16" 19 | }, 20 | "devDependencies": { 21 | "graphql": "^16.10.0" 22 | }, 23 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/fetch" 24 | } 25 | -------------------------------------------------------------------------------- /packages/fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OperationVariables, 3 | TypedDocumentNode, 4 | BaseOperations, 5 | OperationData, 6 | } from "@ts-gql/tag"; 7 | import { GraphQLFormattedError } from "graphql/error/GraphQLError"; 8 | import { print } from "graphql/language/printer"; 9 | 10 | export type Fetcher = < 11 | TTypedDocumentNode extends TypedDocumentNode 12 | >( 13 | operation: TTypedDocumentNode, 14 | ...variables: 15 | | [OperationVariables] 16 | | ({} extends OperationVariables ? [] : never) 17 | ) => Promise>; 18 | 19 | export function createFetcher(url: string): Fetcher { 20 | return ((operation: any, variables: any) => { 21 | return fetch(url, { 22 | method: "POST", 23 | headers: { 24 | "Content-Type": "application/json", 25 | }, 26 | body: JSON.stringify({ 27 | query: print(operation), 28 | variables, 29 | }), 30 | }) 31 | .then((res) => res.json()) 32 | .then((data) => { 33 | if (data.errors?.length) { 34 | throw new GraphQLErrorResult(data.data, data.errors); 35 | } 36 | return data.data; 37 | }); 38 | }) as any; 39 | } 40 | 41 | export class GraphQLErrorResult extends Error { 42 | errors: readonly GraphQLFormattedError[]; 43 | data: unknown; 44 | constructor(data: unknown, errors: readonly GraphQLFormattedError[]) { 45 | super( 46 | `GraphQL errors occurred:\n${errors.map((e) => e.message).join("\n")})}` 47 | ); 48 | this.errors = errors; 49 | this.data = data; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/next/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/next 2 | 3 | ## 17.0.1 4 | 5 | ### Patch Changes 6 | 7 | - [`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad) Thanks [@emmatown](https://github.com/emmatown)! - Add `exports` field to `package.json` 8 | 9 | - Updated dependencies [[`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad)]: 10 | - @ts-gql/compiler@0.16.5 11 | 12 | ## 17.0.0 13 | 14 | ### Patch Changes 15 | 16 | - Updated dependencies [[`ec3330f`](https://github.com/Thinkmill/ts-gql/commit/ec3330fd2b7e1e47162773a065bb76e3c11ed072), [`11d562a`](https://github.com/Thinkmill/ts-gql/commit/11d562aa0074f6b4a5896bb2f4f45cf16dd61fd0), [`11d562a`](https://github.com/Thinkmill/ts-gql/commit/11d562aa0074f6b4a5896bb2f4f45cf16dd61fd0)]: 17 | - @ts-gql/compiler@0.16.0 18 | 19 | ## 16.0.3 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [[`bc8d6eb`](https://github.com/Thinkmill/ts-gql/commit/bc8d6ebbf1021829de24d3c916dad5e0b3ab1edf)]: 24 | - @ts-gql/compiler@0.15.3 25 | 26 | ## 16.0.2 27 | 28 | ### Patch Changes 29 | 30 | - [`5b800e7`](https://github.com/Thinkmill/ts-gql/commit/5b800e763cb428c972ff0bfb85592405bb513754) Thanks [@emmatown](https://github.com/emmatown)! - Republish after broken release 31 | 32 | - Updated dependencies [[`5b800e7`](https://github.com/Thinkmill/ts-gql/commit/5b800e763cb428c972ff0bfb85592405bb513754)]: 33 | - @ts-gql/compiler@0.15.2 34 | 35 | ## 16.0.1 36 | 37 | ### Patch Changes 38 | 39 | - [`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@16` 40 | 41 | - Updated dependencies [[`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd)]: 42 | - @ts-gql/compiler@0.15.1 43 | 44 | ## 16.0.0 45 | 46 | ### Major Changes 47 | 48 | - [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Added support for `"mode": "no-transform"` and `"mode": "mixed"`. See https://github.com/Thinkmill/ts-gql/blob/main/docs/no-transform.md for more details 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies [[`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d), [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d)]: 53 | - @ts-gql/compiler@0.15.0 54 | 55 | ## 15.0.1 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies [[`0e9f8b4`](https://github.com/Thinkmill/ts-gql/commit/0e9f8b4295b0cdfc4c8f679c79ca9264273d0b5b)]: 60 | - @ts-gql/compiler@0.14.4 61 | 62 | ## 15.0.0 63 | 64 | ### Patch Changes 65 | 66 | - [`3149ffe`](https://github.com/Thinkmill/ts-gql/commit/3149ffe2ffb428273e80451d8a67873073e052c8) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@^15.0.0` 67 | 68 | - Updated dependencies [[`3b426f3`](https://github.com/Thinkmill/ts-gql/commit/3b426f3ca4124ffc63f25cb79dab639d5b7db7a1), [`b7e2775`](https://github.com/Thinkmill/ts-gql/commit/b7e2775618dc8ffbf320a02c01706a97933c7458), [`3149ffe`](https://github.com/Thinkmill/ts-gql/commit/3149ffe2ffb428273e80451d8a67873073e052c8)]: 69 | - @ts-gql/compiler@0.14.0 70 | 71 | ## 14.0.0 72 | 73 | ### Major Changes 74 | 75 | - [`ab25d45`](https://github.com/Thinkmill/ts-gql/commit/ab25d45bd80dfe58f878a500c92e0bdb3eef5c86) Thanks [@emmatown](https://github.com/emmatown)! - Remove automatic insertion of `@ts-gql/babel-plugin`, you should include it yourself. Automatically including the babel plugin caused confusion when the code would run with Next but would not run when using another tool like Jest or etc. 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies [[`90d1567`](https://github.com/Thinkmill/ts-gql/commit/90d15672f4737d8a1c15429f680790c9abdccf58), [`2df884a`](https://github.com/Thinkmill/ts-gql/commit/2df884a168c5e4285956f70ff10bb70f80704484), [`19fd7d9`](https://github.com/Thinkmill/ts-gql/commit/19fd7d98c4bb0a290f1cfe831608a5c13f498b22), [`d6d5594`](https://github.com/Thinkmill/ts-gql/commit/d6d55946c9dfc118d87ba34b79d48d48a3144e4d), [`ccf3770`](https://github.com/Thinkmill/ts-gql/commit/ccf37705e7f58a31906c9b96dbd27ded2447d817)]: 80 | - @ts-gql/compiler@0.13.4 81 | 82 | ## 13.0.0 83 | 84 | ### Patch Changes 85 | 86 | - Updated dependencies [[`2c04c58`](https://github.com/Thinkmill/ts-gql/commit/2c04c58c69c0f209ad6c5281e7093686984b6557)]: 87 | - @ts-gql/compiler@0.13.0 88 | 89 | ## 12.0.1 90 | 91 | ### Patch Changes 92 | 93 | - Fix broken release 94 | 95 | - Updated dependencies []: 96 | - @ts-gql/compiler@0.12.1 97 | 98 | ## 12.0.0 99 | 100 | ### Patch Changes 101 | 102 | - Updated dependencies [[`0b8679c`](https://github.com/Thinkmill/ts-gql/commit/0b8679cd9d7e3a47c63071559f344fa22d7aaa64)]: 103 | - @ts-gql/compiler@0.12.0 104 | 105 | ## 11.0.0 106 | 107 | ### Patch Changes 108 | 109 | - Updated dependencies [[`fba7341`](https://github.com/Thinkmill/ts-gql/commit/fba7341a1418e0a9d555172dc5c6e86899fa6ed3)]: 110 | - @ts-gql/compiler@0.11.0 111 | 112 | ## 10.0.0 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies [[`4be8faa`](https://github.com/Thinkmill/ts-gql/commit/4be8faafa0fba17efa491a0aec8ddbb472aa5572)]: 117 | - @ts-gql/compiler@0.10.0 118 | 119 | ## 9.0.0 120 | 121 | ### Patch Changes 122 | 123 | - Updated dependencies [[`f279b23`](https://github.com/Thinkmill/ts-gql/commit/f279b234ca1a264ed675863bccc9eca52b9d12f4)]: 124 | - @ts-gql/compiler@0.9.0 125 | 126 | ## 8.0.0 127 | 128 | ### Patch Changes 129 | 130 | - Updated dependencies [[`e97fd72`](https://github.com/Thinkmill/ts-gql/commit/e97fd72bc779c1804eddc34238aab57ffb63c9d7)]: 131 | - @ts-gql/compiler@0.8.0 132 | 133 | ## 7.0.0 134 | 135 | ### Patch Changes 136 | 137 | - Updated dependencies [[`1acfb89`](https://github.com/Thinkmill/ts-gql/commit/1acfb89b8aca3db55a5a583eac57bd26654e54b1)]: 138 | - @ts-gql/compiler@0.7.0 139 | 140 | ## 6.0.0 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies [[`d798897`](https://github.com/Thinkmill/ts-gql/commit/d7988972e801c41bb96aaa4dec5763ebae73e30e)]: 145 | - @ts-gql/compiler@0.6.0 146 | 147 | ## 5.0.0 148 | 149 | ### Patch Changes 150 | 151 | - Updated dependencies [[`caa1974`](https://github.com/Thinkmill/ts-gql/commit/caa19743de1aa1345795691b8d4eea58c052fc8f)]: 152 | - @ts-gql/compiler@0.5.0 153 | 154 | ## 4.0.0 155 | 156 | ### Major Changes 157 | 158 | - [`987ae27`](https://github.com/Thinkmill/ts-gql/commit/987ae27ec21cfcd8d35d829385c1220431fc295b) Thanks [@emmatown](https://github.com/emmatown)! - Exit the process when ts-gql fails with a fatal error(this means something has gone very wrong, not that a user has a syntax error, a GraphQL error or etc.) 159 | 160 | ### Patch Changes 161 | 162 | - Updated dependencies [[`e0cdba4`](https://github.com/Thinkmill/ts-gql/commit/e0cdba40c84c522845e860bec694d837bfaec684), [`ef7a2fe`](https://github.com/Thinkmill/ts-gql/commit/ef7a2fec4b05b7a9b2622ccf5e5e7d5f564311ea), [`987ae27`](https://github.com/Thinkmill/ts-gql/commit/987ae27ec21cfcd8d35d829385c1220431fc295b)]: 163 | - @ts-gql/compiler@0.4.0 164 | 165 | ## 3.0.0 166 | 167 | ### Patch Changes 168 | 169 | - Updated dependencies [[`abba421`](https://github.com/Thinkmill/ts-gql/commit/abba4214b10bc878de9c7c9e350e5ef04f3ef11f), [`e42383b`](https://github.com/Thinkmill/ts-gql/commit/e42383b5970a554462384f9851aabc173f7fcf52)]: 170 | - @ts-gql/compiler@0.3.0 171 | 172 | ## 2.0.0 173 | 174 | ### Patch Changes 175 | 176 | - Updated dependencies [[`b83e180`](https://github.com/Thinkmill/ts-gql/commit/b83e180ea94cd7fb1d66d5c7835f333a5fcf56f5)]: 177 | - @ts-gql/compiler@0.2.0 178 | 179 | ## 1.0.0 180 | 181 | ### Minor Changes 182 | 183 | - [`e4c60ad`](https://github.com/Thinkmill/ts-gql/commit/e4c60adcc45abba018c4b9d4d0379e7d529a9af1) Thanks [@emmatown](https://github.com/emmatown)! - Initial release 184 | 185 | ### Patch Changes 186 | 187 | - Updated dependencies [[`e4c60ad`](https://github.com/Thinkmill/ts-gql/commit/e4c60adcc45abba018c4b9d4d0379e7d529a9af1), [`e4c60ad`](https://github.com/Thinkmill/ts-gql/commit/e4c60adcc45abba018c4b9d4d0379e7d529a9af1)]: 188 | - @ts-gql/compiler@0.1.0 189 | - @ts-gql/babel-plugin@0.1.0 190 | -------------------------------------------------------------------------------- /packages/next/README.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/next 2 | 3 | `@ts-gql/next` starts ts-gql's watcher when you start Next.js's dev server. Note that it **does not generate the artifacts when running `next build`**, you should run `ts-gql build` in a script before running `next build`. 4 | 5 | ## Usage 6 | 7 | ```js 8 | const { withTsGql } = require("@ts-gql/next"); 9 | module.exports = withTsGql({ ...yourOwnConfig }); 10 | ``` 11 | -------------------------------------------------------------------------------- /packages/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/next", 3 | "version": "17.0.1", 4 | "main": "dist/next.cjs.js", 5 | "module": "dist/next.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/next.esm.js", 9 | "default": "./dist/next.cjs.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@babel/runtime": "^7.9.2" 16 | }, 17 | "peerDependencies": { 18 | "@ts-gql/compiler": "*", 19 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16" 20 | }, 21 | "devDependencies": { 22 | "@ts-gql/compiler": "*", 23 | "graphql": "^16.10.0" 24 | }, 25 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/next" 26 | } 27 | -------------------------------------------------------------------------------- /packages/next/src/index.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "@ts-gql/compiler"; 2 | 3 | export const withTsGql = 4 | (internalConfig: any = {}) => 5 | ( 6 | phase: 7 | | "phase-export" 8 | | "phase-production-build" 9 | | "phase-production-server" 10 | | "phase-development-server", 11 | thing: any 12 | ) => { 13 | if (phase === "phase-development-server") { 14 | watch(process.cwd()).catch((err) => { 15 | console.error(err.toString()); 16 | process.exit(1); 17 | }); 18 | } 19 | let internalConfigObj = 20 | typeof internalConfig === "function" 21 | ? internalConfig(phase, thing) 22 | : internalConfig; 23 | return internalConfigObj; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/schema/README.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/schema 2 | 3 | This package has been renamed to `@graphql-ts/schema` and moved to https://github.com/Thinkmill/graphql-ts 4 | -------------------------------------------------------------------------------- /packages/tag/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ts-gql/tag 2 | 3 | ## 0.7.3 4 | 5 | ### Patch Changes 6 | 7 | - [#125](https://github.com/Thinkmill/ts-gql/pull/125) [`65adda4`](https://github.com/Thinkmill/ts-gql/commit/65adda40943ff8fc459ea152e76cf4d15d9ed4e5) Thanks [@emmatown](https://github.com/emmatown)! - Fix importing in TypeScript module resolution modes that don't respect the `exports` field 8 | 9 | ## 0.7.2 10 | 11 | ### Patch Changes 12 | 13 | - [`2ecd4a5`](https://github.com/Thinkmill/ts-gql/commit/2ecd4a569ca7964cce876d24d73b901c14d4526a) Thanks [@emmatown](https://github.com/emmatown)! - Fix types 14 | 15 | ## 0.7.1 16 | 17 | ### Patch Changes 18 | 19 | - [`74154ca`](https://github.com/Thinkmill/ts-gql/commit/74154ca7dab4ea3bd03ff5da5105ca770a63afad) Thanks [@emmatown](https://github.com/emmatown)! - Add `exports` field to `package.json` 20 | 21 | ## 0.7.0 22 | 23 | ### Minor Changes 24 | 25 | - [#98](https://github.com/Thinkmill/ts-gql/pull/98) [`bc8d6eb`](https://github.com/Thinkmill/ts-gql/commit/bc8d6ebbf1021829de24d3c916dad5e0b3ab1edf) Thanks [@emmatown](https://github.com/emmatown)! - `ts-gql`'s `TypedDocumentNode` type is now compatible with [`@graphql-typed-document-node/core`](https://github.com/dotansimha/graphql-typed-document-node)'s `TypedDocumentNode`. 26 | 27 | The recommended usage of ts-gql with Apollo Client is now to use `@apollo/client` directly. This also allows ts-gql to be used with urql and any other GraphQL client that supports `@graphql-typed-document-node/core`. The `@ts-gql/apollo` package can still be used and may be updated in the future to avoid breakage if that makes sense but it is no longer the recommended pattern. 28 | 29 | When using `@apollo/client` over `@ts-gql/apollo`, it's important to note that some type safety will be lost: 30 | 31 | - Variables are always optional so omitting variables when they are required will no longer be caught by TypeScript 32 | - `refetchQueries` will accept any string so passing names to queries that don't exist will not cause a TypeScript error. You should likely pass in the document with the query itself to avoid mis-typing query names causing errors. 33 | 34 | Because `@graphql-typed-document-node/core`'s `TypedDocumentNode` extends `graphql`'s `DocumentNode`, this means that `getDocumentNode` from `@ts-gql/tag` is no longer necessary. This could be another cause for bugs if there are two APIs, one that accepts a `TypedDocumentNode` that you should use and another that accepts `DocumentNode` which you shouldn't use, you could accidentally use the API that accepts `DocumentNode` over the one that accepts `TypedDocumentNode` where previously you would get an error when passing a `TypedDocumentNode` to something accepting a `DocumentNode`. 35 | 36 | #### Context behind this change 37 | 38 | When ts-gql was originally written, `@graphql-typed-document-node/core` did not exist. Since then, `@graphql-typed-document-node/core` has become used by Apollo Client and urql. Given that, maintaining types to adapt Apollo Client to ts-gql's `TypedDocumentNode` seems less sensible. 39 | 40 | While this does mean that some of ts-gql's safety is reduced, this seems like an appropriate trade-off so that ts-gql can reduce maintaince burden, avoid imposing opinions on top of GraphQL clients and support more GraphQL clients without having to write types for them specifically. 41 | 42 | ## 0.6.1 43 | 44 | ### Patch Changes 45 | 46 | - [`0e3e2f5`](https://github.com/Thinkmill/ts-gql/commit/0e3e2f5004c7e42bbc394664c5e667ce3597e6fd) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@16` 47 | 48 | ## 0.6.0 49 | 50 | ### Minor Changes 51 | 52 | - [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Removed unused `Documents` export. 53 | 54 | * [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Added `name` property to `BaseTypedFragment` and `fragments` property to `BaseTypedDocument` 55 | 56 | - [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Added support for `"mode": "no-transform"` and `"mode": "mixed"`. See https://github.com/Thinkmill/ts-gql/blob/main/docs/no-transform.md for more details 57 | 58 | * [#90](https://github.com/Thinkmill/ts-gql/pull/90) [`dc22e45`](https://github.com/Thinkmill/ts-gql/commit/dc22e457d14c816274037010a627d10bcb30f11d) Thanks [@emmatown](https://github.com/emmatown)! - Removed spread parameter from `gql` export to align with the fact that `gql` from the `@ts-gql/tag` entrypoint does not allow interpolations. 59 | 60 | ## 0.5.3 61 | 62 | ### Patch Changes 63 | 64 | - [`3149ffe`](https://github.com/Thinkmill/ts-gql/commit/3149ffe2ffb428273e80451d8a67873073e052c8) Thanks [@emmatown](https://github.com/emmatown)! - Support `graphql@^15.0.0` 65 | 66 | ## 0.5.2 67 | 68 | ### Patch Changes 69 | 70 | - [`ab25d45`](https://github.com/Thinkmill/ts-gql/commit/ab25d45bd80dfe58f878a500c92e0bdb3eef5c86) Thanks [@emmatown](https://github.com/emmatown)! - Improve error message when `gql` is called at runtime 71 | 72 | ## 0.5.1 73 | 74 | ### Patch Changes 75 | 76 | - [`89bbe8b`](https://github.com/Thinkmill/ts-gql/commit/89bbe8bbd8c3ed3fd3d42ccb5fb0bfacb0d15575) Thanks [@emmatown](https://github.com/emmatown)! - Fix the type of the `documents` field 77 | 78 | ## 0.5.0 79 | 80 | ### Minor Changes 81 | 82 | - [`abba421`](https://github.com/Thinkmill/ts-gql/commit/abba4214b10bc878de9c7c9e350e5ef04f3ef11f) Thanks [@emmatown](https://github.com/emmatown)! - Include all documents on typed document nodes 83 | 84 | ## 0.4.0 85 | 86 | ### Minor Changes 87 | 88 | - [`b83e180`](https://github.com/Thinkmill/ts-gql/commit/b83e180ea94cd7fb1d66d5c7835f333a5fcf56f5) Thanks [@emmatown](https://github.com/emmatown)! - Rename some types 89 | 90 | ## 0.3.0 91 | 92 | ### Minor Changes 93 | 94 | - [`e4c60ad`](https://github.com/Thinkmill/ts-gql/commit/e4c60adcc45abba018c4b9d4d0379e7d529a9af1) Thanks [@emmatown](https://github.com/emmatown)! - Use new technique to generate types. 95 | 96 | This requires you to use `@ts-gql/compiler` and `@ts-gql/babel-plugin` in addition to `@ts-gql/eslint-plugin`. 97 | 98 | Configuration also now lives in the `package.json` like this: 99 | 100 | ```json 101 | { 102 | "ts-gql": { 103 | "schema": "schema.graphql" 104 | } 105 | } 106 | ``` 107 | 108 | ## 0.2.0 109 | 110 | ### Minor Changes 111 | 112 | - [`8485b1a`](https://github.com/Thinkmill/ts-gql/commit/8485b1a28228feea836d076cc7dd1a0691414248) Thanks [@emmatown](https://github.com/emmatown)! - Remove -with-required-variables types 113 | 114 | ## 0.1.0 115 | 116 | ### Minor Changes 117 | 118 | - [`b444283`](https://github.com/Thinkmill/ts-gql/commit/b44428353e6e94f7df60b8ffc409b44b6fbca1ca) Thanks [@emmatown](https://github.com/emmatown)! - Initial release 119 | -------------------------------------------------------------------------------- /packages/tag/no-transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/tag.cjs.js", 3 | "module": "dist/tag.esm.js" 4 | } 5 | -------------------------------------------------------------------------------- /packages/tag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/tag", 3 | "version": "0.7.3", 4 | "main": "dist/tag.cjs.js", 5 | "module": "dist/tag.esm.js", 6 | "exports": { 7 | ".": { 8 | "module": "./dist/tag.esm.js", 9 | "default": "./dist/tag.cjs.js" 10 | }, 11 | "./no-transform": { 12 | "module": "./no-transform/dist/tag.esm.js", 13 | "default": "./no-transform/dist/tag.cjs.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "@graphql-typed-document-node/core": "^3.1.1", 20 | "graphql-tag": "^2.12.6" 21 | }, 22 | "peerDependencies": { 23 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16" 24 | }, 25 | "devDependencies": { 26 | "graphql": "^16.10.0" 27 | }, 28 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/packages/tag", 29 | "preconstruct": { 30 | "entrypoints": [ 31 | "index.js", 32 | "no-transform.js" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/tag/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from "graphql"; 2 | 3 | // TODO: subscriptions 4 | 5 | export type BaseTypedDocument = { 6 | result: any; 7 | documents: Record>; 8 | fragments: (a: any) => any; 9 | }; 10 | 11 | export type BaseTypedQuery = BaseTypedDocument & { 12 | type: "query"; 13 | variables: any; 14 | }; 15 | 16 | export type BaseTypedMutation = BaseTypedDocument & { 17 | type: "mutation"; 18 | variables: any; 19 | }; 20 | 21 | export type BaseOperations = BaseTypedQuery | BaseTypedMutation; 22 | 23 | export type BaseTypedFragment = BaseTypedDocument & { 24 | name: string; 25 | type: "fragment"; 26 | }; 27 | 28 | export type BaseDocumentTypes = BaseOperations | BaseTypedFragment; 29 | 30 | export type TypedDocumentNode = 31 | DocumentNode & { 32 | ___type: TypedDocument; 33 | } & (TypedDocument extends BaseOperations 34 | ? import("@graphql-typed-document-node/core").TypedDocumentNode< 35 | TypedDocument["result"], 36 | TypedDocument["variables"] 37 | > 38 | : unknown); 39 | 40 | export type OperationData> = 41 | Node["___type"]["result"]; 42 | 43 | export type OperationVariables> = 44 | Node["___type"]["variables"]; 45 | 46 | export type FragmentData> = 47 | Node["___type"]["result"]; 48 | 49 | export type AllDocuments> = 50 | Node["___type"]["documents"]; 51 | 52 | export declare function gql(strings: TemplateStringsArray): never; 53 | 54 | /** 55 | * @deprecated {@linkcode TypedDocumentNode} is now assignable to {@linkcode DocumentNode} 56 | * so this call can be removed and the {@linkcode TypedDocumentNode} can be used directly 57 | */ 58 | export function getDocumentNode( 59 | node: TypedDocumentNode 60 | ): DocumentNode; 61 | -------------------------------------------------------------------------------- /packages/tag/src/index.js: -------------------------------------------------------------------------------- 1 | export const gql = () => { 2 | if (process.env.NODE_ENV !== "production") { 3 | throw new Error( 4 | "Unexpected runtime `gql` call. `gql` from `@ts-gql/tag` should never be called at runtime. This is likely happening because:\n- You haven't included `@ts-gql/babel-plugin` in your Babel config\n- This call doesn't have `as import(...)` that should be added by `@ts-gql/eslint-plugin`" 5 | ); 6 | } else { 7 | throw new Error("Unexpected runtime `gql` call."); 8 | } 9 | }; 10 | 11 | export const getDocumentNode = (node) => node; 12 | -------------------------------------------------------------------------------- /packages/tag/src/no-transform.d.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | AllDocuments, 3 | BaseDocumentTypes, 4 | BaseOperations, 5 | BaseTypedDocument, 6 | BaseTypedFragment, 7 | BaseTypedMutation, 8 | BaseTypedQuery, 9 | FragmentData, 10 | OperationData, 11 | OperationVariables, 12 | TypedDocumentNode, 13 | } from "."; 14 | export { getDocumentNode } from "."; 15 | import { TypedDocumentNode, BaseTypedFragment } from "."; 16 | 17 | type ProvidedFragments = (requiredFragments: T) => T; 18 | 19 | type TypedDocumentNodeToBeCast = { 20 | kind: unknown; 21 | definitions: unknown; 22 | ___type: { 23 | type: "query" | "fragment" | "mutation"; 24 | result: any; 25 | variables: any; 26 | documents: any; 27 | name: any; 28 | fragments: ProvidedFragments< 29 | [Fragment] extends [never] 30 | ? "none" 31 | : { 32 | [Key in Fragment]: true; 33 | } 34 | >; 35 | }; 36 | }; 37 | 38 | export declare function gql< 39 | Fragments extends TypedDocumentNode[] 40 | >( 41 | strings: TemplateStringsArray, 42 | ...fragments: [...Fragments] 43 | ): TypedDocumentNodeToBeCast; 44 | -------------------------------------------------------------------------------- /packages/tag/src/no-transform.js: -------------------------------------------------------------------------------- 1 | export { default as gql } from "graphql-tag"; 2 | 3 | export const getDocumentNode = (node) => node; 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "test-app" 4 | -------------------------------------------------------------------------------- /test-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["@ts-gql"], 3 | rules: { 4 | "@ts-gql/ts-gql": "error", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /test-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /test-app/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@ts-gql/next").withTsGql(); 2 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-gql/test-app", 3 | "version": "0.1.30", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "ts-gql build" 8 | }, 9 | "dependencies": { 10 | "@apollo/client": "^3.6.9", 11 | "@ts-gql/babel-plugin": "^0.1.0", 12 | "@ts-gql/compiler": "^0.16.0", 13 | "@ts-gql/eslint-plugin": "^0.9.0", 14 | "@ts-gql/fetch": "^0.1.2", 15 | "@ts-gql/next": "^17.0.0", 16 | "@ts-gql/tag": "^0.7.0", 17 | "@types/node": "^22.13.1", 18 | "@types/react": "^16.9.43", 19 | "@types/react-dom": "^16.9.8", 20 | "@typescript-eslint/parser": "^6.3.0", 21 | "next": "^12.0.3", 22 | "raw-loader": "^4.0.1", 23 | "react": "^16.14.0", 24 | "react-dom": "^16.14.0", 25 | "urql": "^2.2.0" 26 | }, 27 | "ts-gql": { 28 | "schema": "schema.graphql", 29 | "mode": "mixed" 30 | }, 31 | "repository": "https://github.com/Thinkmill/ts-gql/tree/main/test-app" 32 | } 33 | -------------------------------------------------------------------------------- /test-app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AppProps } from "next/app"; 3 | 4 | export default ({ Component, pageProps }: AppProps) => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /test-app/pages/apollo.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag/no-transform"; 2 | import { createFetcher } from "@ts-gql/fetch"; 3 | import * as urql from "urql"; 4 | import * as apollo from "@apollo/client"; 5 | 6 | const query2 = gql` 7 | query MyQueryApollo($thing: String) { 8 | optional(thing: $thing) 9 | } 10 | ` as import("../__generated__/ts-gql/MyQueryApollo").type; 11 | 12 | const someFragment = gql` 13 | fragment Something2Apollo_x on Query { 14 | hello 15 | someObj { 16 | id 17 | other 18 | } 19 | arr { 20 | id 21 | other 22 | } 23 | thing { 24 | id 25 | ... on ImplementationOfThing { 26 | something 27 | } 28 | } 29 | } 30 | ` as import("../__generated__/ts-gql/Something2Apollo_x").type; 31 | 32 | const otherFragment = gql` 33 | fragment OtherFragment_x on Query { 34 | hello 35 | someObj { 36 | id 37 | other 38 | } 39 | arr { 40 | id 41 | other 42 | } 43 | thing { 44 | id 45 | ... on ImplementationOfThing { 46 | something 47 | } 48 | } 49 | } 50 | ` as import("../__generated__/ts-gql/OtherFragment_x").type; 51 | 52 | console.log(otherFragment); 53 | 54 | const query = gql` 55 | query SomeQueryApollo($arg: String!) { 56 | optional(thing: $arg) 57 | ye: something 58 | 59 | ...Something2Apollo_x 60 | } 61 | ${someFragment} 62 | ` as import("../__generated__/ts-gql/SomeQueryApollo").type; 63 | 64 | let someMutation = gql` 65 | mutation SomeMutationApollo($arg: String!) { 66 | optional(thing: $arg) 67 | ye: something 68 | } 69 | ` as import("../__generated__/ts-gql/SomeMutationApollo").type; 70 | 71 | console.log({ query, someMutation }); 72 | 73 | const fetchGraphQL = createFetcher(""); 74 | 75 | fetchGraphQL(someMutation, { arg: "" }); 76 | fetchGraphQL(query2); 77 | 78 | export default () => { 79 | // let client = useApolloClient(); 80 | // const { data } = useQuery(query, { variables: { arg: "" } }); 81 | // ; 82 | // data.hello; 83 | // data.another; 84 | const [stuff, mutate] = urql.useMutation(someMutation); 85 | mutate(); 86 | mutate({ 87 | arg: "", 88 | }).then((x) => { 89 | x.data?.__typename; 90 | }); 91 | 92 | stuff.data?.__typename; 93 | 94 | const [apolloMutate, apolloStuff] = apollo.useMutation(someMutation, { 95 | variables: { 96 | arg: "", 97 | }, 98 | }); 99 | apolloStuff.data?.optional; 100 | 101 | apolloMutate({ 102 | variables: { 103 | arg: "", 104 | }, 105 | }); 106 | 107 | // data.hello; 108 | // data.other; 109 | // data.aTh; 110 | // data.aThin; 111 | 112 | return "something"; 113 | }; 114 | -------------------------------------------------------------------------------- /test-app/pages/thing/thing.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from "@ts-gql/tag/no-transform"; 2 | import { ApolloClient, InMemoryCache, useQuery } from "@apollo/client"; 3 | import { useMemo } from "react"; 4 | 5 | const postListFragment = gql` 6 | fragment PostList_posts on Post { 7 | title 8 | author { 9 | name 10 | } 11 | } 12 | ` as import("../../__generated__/ts-gql/PostList_posts").type; 13 | 14 | const authorListFragment = gql` 15 | fragment AuthorList_author on Author { 16 | name 17 | } 18 | ` as import("../../__generated__/ts-gql/AuthorList_author").type; 19 | 20 | console.log(authorListFragment); 21 | 22 | const query = gql` 23 | query PostListPage { 24 | posts { 25 | title 26 | ...PostList_posts 27 | } 28 | } 29 | ${postListFragment} 30 | ` as import("../../__generated__/ts-gql/PostListPage").type; 31 | 32 | export default function Test() { 33 | const client = useMemo(() => { 34 | return new ApolloClient({ 35 | uri: "http://localhost:3000/api/graphql", 36 | cache: new InMemoryCache(), 37 | }); 38 | }, []); 39 | const { loading, data, error } = useQuery(query, { client }); 40 | return
{JSON.stringify({ loading, data, error }, null, 2)}
; 41 | } 42 | -------------------------------------------------------------------------------- /test-app/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String! 3 | other: Boolean! 4 | another: String! 5 | something: String 6 | someObj: OutputThing! 7 | arr: [OutputThing!]! 8 | thing: Thing! 9 | optional(thing: String): String! 10 | oneMore(thing: String, other: Something!): String! @deprecated(reason: "Blah") 11 | posts: [Post!]! 12 | } 13 | 14 | type Mutation { 15 | hello: String! 16 | other: Boolean! 17 | another: String! 18 | something: String 19 | optional(thing: String): String! 20 | oneMore(thing: String, other: Something!): String! 21 | } 22 | 23 | interface Thing { 24 | id: ID! 25 | } 26 | 27 | type ImplementationOfThing implements Thing { 28 | id: ID! 29 | something: String! 30 | } 31 | 32 | type OutputThing { 33 | id: ID! 34 | other: String! 35 | } 36 | 37 | input Something { 38 | yes: Boolean 39 | no: String! 40 | } 41 | 42 | type Post { 43 | title: String! 44 | author: Author! 45 | } 46 | 47 | type Author { 48 | name: String! 49 | } 50 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 4 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 5 | "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 6 | "declaration": true /* Generates corresponding '.d.ts' file. */, 7 | "noEmit": true /* Do not emit outputs. */, 8 | "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, 9 | "strict": true /* Enable all strict type-checking options. */, 10 | "moduleResolution": "bundler" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true, 13 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 14 | "skipLibCheck": true, 15 | "useUnknownInCatchVariables": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /types/fixturez.d.ts: -------------------------------------------------------------------------------- 1 | type Opts = { 2 | glob?: string | Array; 3 | root?: string; 4 | cleanup?: boolean; 5 | }; 6 | 7 | declare module "fixturez" { 8 | export default function ( 9 | cwd: string, 10 | opts?: Opts 11 | ): { 12 | find: (fixture: string) => string; 13 | temp: () => string; 14 | copy: (fixture: string) => string; 15 | cleanup: () => void; 16 | }; 17 | } 18 | --------------------------------------------------------------------------------