├── .graphqlrc.yml ├── .yarnrc.yml ├── .gitignore ├── empty.js ├── .eslintrc.js ├── .vscode ├── extensions.json └── settings.json ├── tsconfig.build.json ├── codegen.yml ├── tsconfig.json ├── rollup.config.js ├── .editorconfig ├── demo ├── sources │ └── index.ts └── gql │ ├── index.ts │ └── graphql.ts ├── schema.graphql ├── package.json ├── sources ├── plugin.ts ├── preset.ts └── processSources.ts └── README.md /.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | documents: "demo/**/*.ts" 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: ".yarn/releases/yarn-berry.cjs" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/releases 3 | .pnp.* 4 | /lib 5 | -------------------------------------------------------------------------------- /empty.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugin: () => ``, 3 | }; 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | `@yarnpkg`, 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "rootDir": "sources", 6 | "outDir": "lib", 7 | }, 8 | "include": [ 9 | "sources/**/*", 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true, 8 | "eslint.nodePath": ".yarn/sdks" 9 | } 10 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: ./schema.graphql 2 | documents: './demo/**/*.ts' 3 | 4 | require: 5 | - ts-node/register/transpile-only 6 | 7 | pluckConfig: 8 | skipIndent: true 9 | 10 | generates: 11 | ./demo/gql/: 12 | preset: graphql-typescript-integration 13 | plugins: [graphql-typescript-integration/empty] 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": [ 6 | "es2017" 7 | ], 8 | "module": "commonjs", 9 | "noEmit": true, 10 | "strict": true, 11 | "target": "es2017" 12 | }, 13 | "include": [ 14 | "demo/graphql.d.ts", 15 | "demo/graphtype.d.ts", 16 | "**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import ts from '@rollup/plugin-typescript'; 2 | 3 | // eslint-disable-next-line arca/no-default-export 4 | export default { 5 | input: `./sources/preset.ts`, 6 | 7 | output: [{ 8 | dir: `lib`, 9 | entryFileNames: `[name].mjs`, 10 | format: `es`, 11 | }, { 12 | dir: `lib`, 13 | entryFileNames: `[name].js`, 14 | format: `cjs`, 15 | }], 16 | 17 | plugins: [ 18 | ts({ 19 | tsconfig: `tsconfig.build.json`, 20 | }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome! 2 | 3 | # Mark this as the root editorconfig file 4 | root = true 5 | 6 | # Base ruleset for all files 7 | [*] 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # Override rules for markdown 14 | [*.md] 15 | # trailing whitespace is significant in markdown -> do not remove 16 | trim_trailing_whitespace = false 17 | 18 | [*.patch] 19 | trim_trailing_whitespace = false 20 | insert_final_newline = false 21 | -------------------------------------------------------------------------------- /demo/sources/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | import {gql} from '@app/gql'; 4 | 5 | const {NotFound, Foo, Bar} = gql(`#graphql 6 | fragment TweetFragment on Tweet { 7 | id 8 | } 9 | 10 | query Foo { 11 | Tweets { 12 | ...TweetFragment 13 | } 14 | } 15 | 16 | query Bar { 17 | Tweets { 18 | ...TweetFragment 19 | body 20 | } 21 | } 22 | 23 | subscription TweetSubscription { 24 | tweets { 25 | body 26 | } 27 | } 28 | `); 29 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Url 2 | scalar Date 3 | 4 | type Tweet { 5 | id: ID! 6 | body: String 7 | date: Date 8 | Author: User 9 | Stats: Stat 10 | } 11 | 12 | type User { 13 | id: ID! 14 | username: String 15 | first_name: String 16 | last_name: String 17 | full_name: String 18 | name: String @deprecated 19 | avatar_url: Url 20 | } 21 | 22 | type Stat { 23 | views: Int 24 | likes: Int 25 | retweets: Int 26 | responses: Int 27 | } 28 | 29 | type Notification { 30 | id: ID 31 | date: Date 32 | type: String 33 | } 34 | 35 | type Meta { 36 | count: Int 37 | } 38 | 39 | type Query { 40 | Tweet(id: ID!): Tweet 41 | Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet] 42 | TweetsMeta: Meta 43 | User(id: ID!): User 44 | Notifications(limit: Int): [Notification] 45 | NotificationsMeta: Meta 46 | } 47 | 48 | type Mutation { 49 | createTweet (body: String): Tweet 50 | deleteTweet(id: ID!): Tweet 51 | markTweetRead(id: ID!): Boolean 52 | } 53 | 54 | type Subscription { 55 | tweets: Tweet 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-typescript-integration", 3 | "version": "1.2.1", 4 | "main": "./sources/preset.ts", 5 | "dependencies": { 6 | "@graphql-codegen/add": "^2.0.2", 7 | "@graphql-codegen/typed-document-node": "^1.18.9", 8 | "@graphql-codegen/typescript": "^1.22.4", 9 | "@graphql-codegen/typescript-operations": "^1.18.3", 10 | "tslib": "^2.3.0" 11 | }, 12 | "devDependencies": { 13 | "@app/gql": "link:./demo/gql", 14 | "@graphql-codegen/cli": "^1.21.6", 15 | "@graphql-codegen/plugin-helpers": "^1.18.7", 16 | "@graphql-tools/utils": "^7.10.0", 17 | "@graphql-typed-document-node/core": "^3.1.0", 18 | "@rollup/plugin-typescript": "^8.2.1", 19 | "@types/node": "^16.0.0", 20 | "@yarnpkg/eslint-config": "^0.3.0-rc.6", 21 | "eslint": "^7.30.0", 22 | "graphql": "^15.5.1", 23 | "rollup": "^2.52.7", 24 | "ts-node": "^10.0.0", 25 | "typescript": "^4.3.5" 26 | }, 27 | "scripts": { 28 | "prepack": "rm -rf lib && rollup -c", 29 | "postpack": "rm -rf lib" 30 | }, 31 | "files": [ 32 | "lib", 33 | "empty.js" 34 | ], 35 | "publishConfig": { 36 | "main": "./lib/preset.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo/gql/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as graphql from './graphql'; 3 | 4 | const documents = { 5 | "#graphql\n fragment TweetFragment on Tweet {\n id\n }\n\n query Foo {\n Tweets {\n ...TweetFragment\n }\n }\n\n query Bar {\n Tweets {\n ...TweetFragment\n body\n }\n }\n\n subscription TweetSubscription {\n tweets {\n body\n }\n }\n": { 6 | TweetFragment: graphql.TweetFragment_X6_FragmentDoc, 7 | Foo: graphql.Foo_X6_Document, 8 | Bar: graphql.Bar_X6_Document, 9 | TweetSubscription: graphql.TweetSubscription_X6_Document, 10 | }, 11 | }; 12 | 13 | export function gql(source: "#graphql\n fragment TweetFragment on Tweet {\n id\n }\n\n query Foo {\n Tweets {\n ...TweetFragment\n }\n }\n\n query Bar {\n Tweets {\n ...TweetFragment\n body\n }\n }\n\n subscription TweetSubscription {\n tweets {\n body\n }\n }\n"): (typeof documents)["#graphql\n fragment TweetFragment on Tweet {\n id\n }\n\n query Foo {\n Tweets {\n ...TweetFragment\n }\n }\n\n query Bar {\n Tweets {\n ...TweetFragment\n body\n }\n }\n\n subscription TweetSubscription {\n tweets {\n body\n }\n }\n"]; 14 | 15 | export function gql(source: string): unknown; 16 | export function gql(source: string) { 17 | return (documents as any)[source] ?? {}; 18 | } 19 | -------------------------------------------------------------------------------- /sources/plugin.ts: -------------------------------------------------------------------------------- 1 | import {PluginFunction} from '@graphql-codegen/plugin-helpers'; 2 | 3 | import {SourceWithOperations} from './processSources'; 4 | 5 | const SUFFIXES = new Map([ 6 | [`OperationDefinition`, `Document`], 7 | [`FragmentDefinition`, `FragmentDoc`], 8 | ] as const); 9 | 10 | export const plugin: PluginFunction<{ 11 | sourcesWithOperations: Array; 12 | }> = (schema, sources, {sourcesWithOperations}) => [ 13 | `import * as graphql from './graphql';\n`, 14 | `\n`, 15 | ...getDocumentRegistryChunk(sourcesWithOperations), 16 | `\n`, 17 | ...getGqlOverloadChunk(sourcesWithOperations), 18 | `\n`, 19 | `export function gql(source: string): unknown;\n`, 20 | `export function gql(source: string) {\n`, 21 | ` return (documents as any)[source] ?? {};\n`, 22 | `}\n`, 23 | ].join(``); 24 | 25 | function getDocumentRegistryChunk(sourcesWithOperations: Array) { 26 | const lines: Array = []; 27 | lines.push(`const documents = {\n`); 28 | 29 | for (const {source: {rawSDL}, operations} of sourcesWithOperations) { 30 | lines.push(` ${JSON.stringify(rawSDL!)}: {\n`); 31 | 32 | for (const {initialName, uniqueName, definition} of operations) 33 | lines.push(` ${initialName}: graphql.${uniqueName}${SUFFIXES.get(definition.kind)},\n`); 34 | 35 | 36 | lines.push(` },\n`); 37 | } 38 | 39 | lines.push(`};\n`); 40 | 41 | return lines; 42 | } 43 | 44 | function getGqlOverloadChunk(sourcesWithOperations: Array) { 45 | const lines: Array = []; 46 | 47 | // We intentionally don't use a generic, because TS 48 | // would print very long `gql` function signatures (duplicating the source). 49 | for (const {source: {rawSDL}} of sourcesWithOperations) 50 | lines.push(`export function gql(source: ${JSON.stringify(rawSDL!)}): (typeof documents)[${JSON.stringify(rawSDL!)}];\n`); 51 | 52 | return lines; 53 | } 54 | -------------------------------------------------------------------------------- /sources/preset.ts: -------------------------------------------------------------------------------- 1 | import * as addPlugin from '@graphql-codegen/add'; 2 | import {Types} from '@graphql-codegen/plugin-helpers'; 3 | import * as typedDocumentNodePlugin from '@graphql-codegen/typed-document-node'; 4 | import * as typescriptOperationPlugin from '@graphql-codegen/typescript-operations'; 5 | import * as typescriptPlugin from '@graphql-codegen/typescript'; 6 | 7 | import * as dtsGenPlugin from './plugin'; 8 | import {processSources} from './processSources'; 9 | 10 | export const preset: Types.OutputPreset<{ 11 | packageName: string; 12 | }> = { 13 | buildGeneratesSection: options => { 14 | if (!options.schemaAst) 15 | throw new Error(`Missing schema AST`); 16 | 17 | const packageName = options.presetConfig.packageName ?? `@app/gql`; 18 | 19 | const sourcesWithOperations = processSources(options.documents, {schema: options.schemaAst}); 20 | const sources = sourcesWithOperations.map(({source}) => source); 21 | 22 | const pluginMap = { 23 | ...options.pluginMap, 24 | [`add`]: addPlugin, 25 | [`typescript`]: typescriptPlugin, 26 | [`typescript-operations`]: typescriptOperationPlugin, 27 | [`typed-document-node`]: typedDocumentNodePlugin, 28 | [`gen-dts`]: dtsGenPlugin, 29 | }; 30 | 31 | const plugins: Array = [ 32 | {[`add`]: {content: `/* eslint-disable */`}}, 33 | {[`typescript`]: {}}, 34 | {[`typescript-operations`]: {}}, 35 | {[`typed-document-node`]: {}}, 36 | ...options.plugins, 37 | ]; 38 | 39 | const genDtsPlugins: Array = [ 40 | {[`add`]: {content: `/* eslint-disable */`}}, 41 | {[`gen-dts`]: {sourcesWithOperations, packageName}}, 42 | ]; 43 | 44 | return [{ 45 | filename: `${options.baseOutputDir}/graphql.ts`, 46 | plugins, 47 | pluginMap, 48 | schema: options.schema, 49 | config: options.config, 50 | documents: sources, 51 | }, { 52 | filename: `${options.baseOutputDir}/index.ts`, 53 | plugins: genDtsPlugins, 54 | pluginMap, 55 | schema: options.schema, 56 | config: options.config, 57 | documents: sources, 58 | }]; 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /sources/processSources.ts: -------------------------------------------------------------------------------- 1 | import type {Source} from '@graphql-tools/utils'; 2 | import crypto from 'crypto'; 3 | import {DocumentNode, FragmentDefinitionNode, GraphQLObjectType, GraphQLSchema, OperationDefinitionNode, TypeInfo, visit, visitWithTypeInfo} from 'graphql'; 4 | 5 | export type Operation = { 6 | initialName: string; 7 | uniqueName: string; 8 | definition: OperationDefinitionNode | FragmentDefinitionNode; 9 | }; 10 | 11 | export type SourceWithOperations = { 12 | source: Source; 13 | operations: Array; 14 | }; 15 | 16 | // Graphile automatically adds `id` fields to the top-level types 17 | const DISABLED_AUTO_ID_TYPES = new Set([ 18 | `Mutation`, 19 | `Query`, 20 | `Subscription`, 21 | ]); 22 | 23 | type Writeable = { -readonly [P in keyof T]: T[P] }; 24 | const makeWriteable = (val: T): Writeable => val as any; 25 | 26 | export function processSources(sources: Array, {schema}: {schema: GraphQLSchema}) { 27 | const typeInfo = new TypeInfo(schema); 28 | 29 | const fullHashes = new Map(); 30 | const filteredSources: Array = []; 31 | 32 | for (const source of sources) { 33 | if (!source) 34 | continue; 35 | 36 | const hash = crypto.createHash(`sha256`).update(source.rawSDL!).digest(`hex`); 37 | if (fullHashes.has(hash)) 38 | continue; 39 | 40 | fullHashes.set(hash, source); 41 | filteredSources.push(source); 42 | } 43 | 44 | let hashLength = 0; 45 | 46 | findHashSize: 47 | for (let t = 1; t <= 32; ++t) { 48 | const seen = new Set(); 49 | for (const hash of fullHashes.keys()) { 50 | const sub = hash.substr(0, t); 51 | if (seen.has(sub)) { 52 | continue findHashSize; 53 | } else { 54 | seen.add(sub); 55 | } 56 | } 57 | 58 | hashLength = t; 59 | break; 60 | } 61 | 62 | const subHashes = new Map(); 63 | for (const [hash, record] of fullHashes) 64 | subHashes.set(hash.substr(0, hashLength), record); 65 | 66 | const sourcesWithOperations: Array = []; 67 | 68 | for (const [hash, source] of subHashes) { 69 | const {document} = source; 70 | if (!document) 71 | continue; 72 | 73 | const operations: Array = []; 74 | const remappedFragments = new Map(); 75 | 76 | for (const definition of document.definitions ?? []) { 77 | if (!definition) 78 | continue; 79 | 80 | if (definition.kind !== `OperationDefinition` && definition.kind !== `FragmentDefinition`) 81 | continue; 82 | 83 | if (definition.name?.kind !== `Name`) 84 | continue; 85 | 86 | // The leading 'X' in front of the hash is to prevent graphql-code-generator 87 | // from accidentally uppercasing the first letter of the hash. 88 | const initialName = definition.name.value; 89 | const uniqueName = `${initialName}_X${hash}_`; 90 | makeWriteable(definition.name).value = uniqueName; 91 | 92 | if (definition.kind === `FragmentDefinition`) 93 | remappedFragments.set(initialName, uniqueName); 94 | 95 | operations.push({ 96 | initialName, 97 | uniqueName, 98 | definition, 99 | }); 100 | } 101 | 102 | if (operations.length === 0) 103 | continue; 104 | 105 | visit(document!, visitWithTypeInfo(typeInfo, { 106 | SelectionSet: node => { 107 | const type = typeInfo.getParentType(); 108 | if (!(type instanceof GraphQLObjectType)) 109 | return; 110 | 111 | if (DISABLED_AUTO_ID_TYPES.has(type.name)) 112 | return; 113 | 114 | const fields = type.getFields(); 115 | if (!Object.prototype.hasOwnProperty.call(fields, `id`)) 116 | return; 117 | 118 | makeWriteable(node.selections).push({ 119 | kind: `Field`, 120 | name: { 121 | kind: `Name`, 122 | value: `id`, 123 | }, 124 | }); 125 | }, 126 | 127 | FragmentSpread: node => { 128 | const initialName = node.name.value; 129 | const uniqueName = remappedFragments.get(initialName); 130 | 131 | if (typeof uniqueName !== `undefined`) { 132 | makeWriteable(node.name).value = uniqueName; 133 | } 134 | }, 135 | })); 136 | 137 | sourcesWithOperations.push({ 138 | source, 139 | operations, 140 | }); 141 | } 142 | 143 | return sourcesWithOperations; 144 | } 145 | -------------------------------------------------------------------------------- /demo/gql/graphql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | /** All built-in and custom scalars, mapped to their actual values */ 9 | export type Scalars = { 10 | ID: string; 11 | String: string; 12 | Boolean: boolean; 13 | Int: number; 14 | Float: number; 15 | Date: any; 16 | Url: any; 17 | }; 18 | 19 | export type Meta = { 20 | __typename?: 'Meta'; 21 | count?: Maybe; 22 | }; 23 | 24 | export type Mutation = { 25 | __typename?: 'Mutation'; 26 | createTweet?: Maybe; 27 | deleteTweet?: Maybe; 28 | markTweetRead?: Maybe; 29 | }; 30 | 31 | 32 | export type MutationCreateTweetArgs = { 33 | body?: InputMaybe; 34 | }; 35 | 36 | 37 | export type MutationDeleteTweetArgs = { 38 | id: Scalars['ID']; 39 | }; 40 | 41 | 42 | export type MutationMarkTweetReadArgs = { 43 | id: Scalars['ID']; 44 | }; 45 | 46 | export type Notification = { 47 | __typename?: 'Notification'; 48 | date?: Maybe; 49 | id?: Maybe; 50 | type?: Maybe; 51 | }; 52 | 53 | export type Query = { 54 | __typename?: 'Query'; 55 | Notifications?: Maybe>>; 56 | NotificationsMeta?: Maybe; 57 | Tweet?: Maybe; 58 | Tweets?: Maybe>>; 59 | TweetsMeta?: Maybe; 60 | User?: Maybe; 61 | }; 62 | 63 | 64 | export type QueryNotificationsArgs = { 65 | limit?: InputMaybe; 66 | }; 67 | 68 | 69 | export type QueryTweetArgs = { 70 | id: Scalars['ID']; 71 | }; 72 | 73 | 74 | export type QueryTweetsArgs = { 75 | limit?: InputMaybe; 76 | skip?: InputMaybe; 77 | sort_field?: InputMaybe; 78 | sort_order?: InputMaybe; 79 | }; 80 | 81 | 82 | export type QueryUserArgs = { 83 | id: Scalars['ID']; 84 | }; 85 | 86 | export type Stat = { 87 | __typename?: 'Stat'; 88 | likes?: Maybe; 89 | responses?: Maybe; 90 | retweets?: Maybe; 91 | views?: Maybe; 92 | }; 93 | 94 | export type Subscription = { 95 | __typename?: 'Subscription'; 96 | tweets?: Maybe; 97 | }; 98 | 99 | export type Tweet = { 100 | __typename?: 'Tweet'; 101 | Author?: Maybe; 102 | Stats?: Maybe; 103 | body?: Maybe; 104 | date?: Maybe; 105 | id: Scalars['ID']; 106 | }; 107 | 108 | export type User = { 109 | __typename?: 'User'; 110 | avatar_url?: Maybe; 111 | first_name?: Maybe; 112 | full_name?: Maybe; 113 | id: Scalars['ID']; 114 | last_name?: Maybe; 115 | /** @deprecated Field no longer supported */ 116 | name?: Maybe; 117 | username?: Maybe; 118 | }; 119 | 120 | export type TweetFragment_X6_Fragment = { __typename?: 'Tweet', id: string }; 121 | 122 | export type Foo_X6_QueryVariables = Exact<{ [key: string]: never; }>; 123 | 124 | 125 | export type Foo_X6_Query = { __typename?: 'Query', Tweets?: Array<{ __typename?: 'Tweet', id: string } | null> | null }; 126 | 127 | export type Bar_X6_QueryVariables = Exact<{ [key: string]: never; }>; 128 | 129 | 130 | export type Bar_X6_Query = { __typename?: 'Query', Tweets?: Array<{ __typename?: 'Tweet', body?: string | null, id: string } | null> | null }; 131 | 132 | export type TweetSubscription_X6_SubscriptionVariables = Exact<{ [key: string]: never; }>; 133 | 134 | 135 | export type TweetSubscription_X6_Subscription = { __typename?: 'Subscription', tweets?: { __typename?: 'Tweet', body?: string | null, id: string } | null }; 136 | 137 | export const TweetFragment_X6_FragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TweetFragment_X6_"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Tweet"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]} as unknown as DocumentNode; 138 | export const Foo_X6_Document = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Foo_X6_"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"Tweets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TweetFragment_X6_"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},...TweetFragment_X6_FragmentDoc.definitions]} as unknown as DocumentNode; 139 | export const Bar_X6_Document = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Bar_X6_"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"Tweets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TweetFragment_X6_"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},...TweetFragment_X6_FragmentDoc.definitions]} as unknown as DocumentNode; 140 | export const TweetSubscription_X6_Document = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"TweetSubscription_X6_"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tweets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-typescript-integration 2 | 3 | This package is a [preset](https://www.graphql-code-generator.com/docs/presets/presets-index) for [graphql-code-generator](https://www.graphql-code-generator.com/). It automatically generates TypeScript definitions for your GraphQL queries and injects the plumbing required to make the gql queries work at runtime. It can be seen as an alternative to [graphql-let](https://github.com/piglovesyou/graphql-let). 4 | 5 | ## Features 6 | 7 | - Automatically adds the `id` field to every GraphQL selection set where `id` would be a valid field. 8 | 9 | - You can (should) keep your GraphQL queries and mutations within your `.tsx` files, close from the components that will use them. However, unlike typical GraphQL type generations, you don't need to separately import the typed operations - they'll be transparently returned by the `gql(...)` function calls. 10 | 11 | - You don't have to bother using unique fragment, query, and mutation names for each file; the preset will automatically hash them if needed. You can still share specific queries with multiple files by simply exporting it from a file that can be imported from (like you would with any other helper function). 12 | 13 | - Integrated with graphql-code-generator, so you can generate other files, benefit from watch mode, etc. 14 | 15 | ## Why? 16 | 17 | I was a bit frustrated to have to import my typed GraphQL hooks from another file, and to leave my `gql` definitions dangling: 18 | 19 | ```ts 20 | import {useFoo} from './generated-hooks'; 21 | 22 | gql` 23 | query Foo { 24 | bar 25 | } 26 | `; 27 | ``` 28 | 29 | So this tool lets you write the same thing, but in a single statement: 30 | 31 | ```ts 32 | const {Foo} = gql(`#graphql 33 | query Foo { 34 | bar 35 | } 36 | `); 37 | ``` 38 | 39 | I also didn't like that each operation had to be uniquely named not only within the same file, but also within the whole app - it felt like a step backward from encapsulation. This is addressed by hashing the operation names under the hood (cf the [generated output](https://github.com/arcanis/graphql-typescript-integration/blob/19a2e6b1f8949f12ab6e5120e002a30f79dbda41/demo/gql/index.ts#L4-L9)). 40 | 41 | Finally, I wanted something relatively easy to setup and maintain because I don't have that much time 😛 42 | 43 | ## Install 44 | 45 | 1. Install the dependencies: 46 | 47 | ``` 48 | yarn add -D graphql-code-generator graphql-typescript-integration @graphql-typed-document-node/core 49 | ``` 50 | 51 | 2. Setup your code generator in `codegen.yml`: 52 | 53 | ```yaml 54 | # Classic graphql-code-generator config. Check their documentation: 55 | # https://www.graphql-code-generator.com/docs/getting-started/codegen-config 56 | 57 | # Important! 58 | pluckConfig: 59 | skipIndent: true 60 | 61 | generates: 62 | ./folder/where/to/generate/types/: 63 | preset: graphql-typescript-integration 64 | # Needed to workaround a graphql-code-generator limitation 65 | plugins: 66 | - graphql-typescript-integration/empty 67 | ``` 68 | 69 | ## Usage 70 | 71 | You can import the `gql` function from the generated folder. For instance, assuming your output folder is a subfolder named "gql": 72 | 73 | ```ts 74 | import {gql} from './gql'; 75 | 76 | const {GetTweets, CreateTweet} = gql(`#graphql 77 | query GetTweets { 78 | Tweets { 79 | id 80 | } 81 | } 82 | 83 | mutation CreateTweet { 84 | CreateTweet(body: "Hello") { 85 | id 86 | } 87 | } 88 | `); 89 | ``` 90 | 91 | If you want to avoid having to refer to the output folder from a relative path, you can easily configure Yarn to do so: 92 | 93 | ``` 94 | yarn add @app/gql@link:./gql 95 | ``` 96 | 97 | This would let you do `import {gql} from '@app/gql'` from anywhere in your codebase. 98 | 99 | ## Limitations 100 | 101 | - ~~You can't use fragments. I don't use them at the moment, so I haven't looked into them. However, given that queries are properly typed, you'll know when you're passing incomplete data around, which will let you add the missing fields to your queries. Not exactly like fragments, but still a decent workaround.~~ 102 | 103 | - You can now use fragments, as long as they're in the same file as the operations that use them. Note that this limitation is temporary, it's just that I haven't needed to use cross-files fragment so far. 104 | 105 | - You must use the `const query = gql(...);` syntax (not the typical tagged template ``const query = gql`...`;`` one). This is because TypeScript isn't currently smart enough to forward tagged template parameters as their literal types (https://github.com/microsoft/TypeScript/issues/29432). 106 | 107 | - You should indicate `#graphql` right after opening the GraphQL source literal (right after the opening quote, before even any line return). This is because otherwise vscode-graphql won't recognize the string as being GraphQL and won't highlight it (they don't detect `gql` statements when used as regular function calls, unlike graphql-code-generator). 108 | 109 | - A contrario, you must **not** use the `/* GraphQL */` magic comment inside the `gql(...)` function call. This is because [graphql-tag-pluck](https://www.graphql-tools.com/docs/graphql-tag-pluck/), the tool extracting these calls, has a bug and will register the document twice - which brings us to the final limitation: 110 | 111 | - You can only have a single `gql(...)` function call per file. This is because graphql-tag-pluck concatenates all documents from a single file into a single one, with no way to split them back. As a result we end up generating a `gql` overload for the composite query, instead of one for each individual document. It's however not a huge deal - just define multiple queries / mutations within the same `gql` call, and use destructuring to access them individually (cf example in the "Usage" section). 112 | 113 | ## License (MIT) 114 | 115 | > **Copyright © 2021 Mael Nison** 116 | > 117 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 118 | > 119 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 120 | > 121 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 122 | --------------------------------------------------------------------------------