From ed35c0899688fbf4a090878bead0218668aaba50 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 17 Aug 2021 20:38:29 +0200 Subject: [PATCH] feat(gql-tag-operations): support module augmentation --- .changeset/lucky-rabbits-build.md | 6 + dev-test/codegen.yml | 6 + .../gql-tag-operations-urql/gql/graphql.ts | 183 ++++++++++++++++++ .../gql-tag-operations-urql/gql/index.d.ts | 25 +++ .../gql-tag-operations-urql/schema.graphql | 52 +++++ dev-test/gql-tag-operations-urql/src/index.ts | 25 +++ .../gql-tag-operations/src/index.ts | 63 ++++-- .../presets/gql-tag-operations/src/index.ts | 28 ++- 8 files changed, 364 insertions(+), 24 deletions(-) create mode 100644 .changeset/lucky-rabbits-build.md create mode 100644 dev-test/gql-tag-operations-urql/gql/graphql.ts create mode 100644 dev-test/gql-tag-operations-urql/gql/index.d.ts create mode 100644 dev-test/gql-tag-operations-urql/schema.graphql create mode 100644 dev-test/gql-tag-operations-urql/src/index.ts diff --git a/.changeset/lucky-rabbits-build.md b/.changeset/lucky-rabbits-build.md new file mode 100644 index 00000000000..30e7c833214 --- /dev/null +++ b/.changeset/lucky-rabbits-build.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/gql-tag-operations': minor +'@graphql-codegen/gql-tag-operations-preset': minor +--- + +feat: support module augumentation for extending the types of gql functions from existing packages via the `augmentedModuleName` config option. diff --git a/dev-test/codegen.yml b/dev-test/codegen.yml index 052b56a4f76..4b0f0f25760 100644 --- a/dev-test/codegen.yml +++ b/dev-test/codegen.yml @@ -392,3 +392,9 @@ generates: schema: ./dev-test/gql-tag-operations/schema.graphql documents: './dev-test/gql-tag-operations/src/**/*.ts' preset: gql-tag-operations-preset + ./dev-test/gql-tag-operations-urql/gql: + schema: ./dev-test/gql-tag-operations-urql/schema.graphql + documents: './dev-test/gql-tag-operations-urql/src/**/*.ts' + preset: gql-tag-operations-preset + presetConfig: + augmentedModuleName: '@urql/core' diff --git a/dev-test/gql-tag-operations-urql/gql/graphql.ts b/dev-test/gql-tag-operations-urql/gql/graphql.ts new file mode 100644 index 00000000000..012dfe50a53 --- /dev/null +++ b/dev-test/gql-tag-operations-urql/gql/graphql.ts @@ -0,0 +1,183 @@ +/* eslint-disable */ +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + Date: any; + Url: any; +}; + +export type Meta = { + __typename?: 'Meta'; + count?: Maybe; +}; + +export type Mutation = { + __typename?: 'Mutation'; + createTweet?: Maybe; + deleteTweet?: Maybe; + markTweetRead?: Maybe; +}; + +export type MutationCreateTweetArgs = { + body?: Maybe; +}; + +export type MutationDeleteTweetArgs = { + id: Scalars['ID']; +}; + +export type MutationMarkTweetReadArgs = { + id: Scalars['ID']; +}; + +export type Notification = { + __typename?: 'Notification'; + id?: Maybe; + date?: Maybe; + type?: Maybe; +}; + +export type Query = { + __typename?: 'Query'; + Tweet?: Maybe; + Tweets?: Maybe>>; + TweetsMeta?: Maybe; + User?: Maybe; + Notifications?: Maybe>>; + NotificationsMeta?: Maybe; +}; + +export type QueryTweetArgs = { + id: Scalars['ID']; +}; + +export type QueryTweetsArgs = { + limit?: Maybe; + skip?: Maybe; + sort_field?: Maybe; + sort_order?: Maybe; +}; + +export type QueryUserArgs = { + id: Scalars['ID']; +}; + +export type QueryNotificationsArgs = { + limit?: Maybe; +}; + +export type Stat = { + __typename?: 'Stat'; + views?: Maybe; + likes?: Maybe; + retweets?: Maybe; + responses?: Maybe; +}; + +export type Tweet = { + __typename?: 'Tweet'; + id: Scalars['ID']; + body?: Maybe; + date?: Maybe; + Author?: Maybe; + Stats?: Maybe; +}; + +export type User = { + __typename?: 'User'; + id: Scalars['ID']; + username?: Maybe; + first_name?: Maybe; + last_name?: Maybe; + full_name?: Maybe; + /** @deprecated Field no longer supported */ + name?: Maybe; + avatar_url?: Maybe; +}; + +export type FooQueryVariables = Exact<{ [key: string]: never }>; + +export type FooQuery = { __typename?: 'Query'; Tweets?: Maybe>> }; + +export type LelFragment = { __typename?: 'Tweet'; id: string; body?: Maybe }; + +export type BarQueryVariables = Exact<{ [key: string]: never }>; + +export type BarQuery = { + __typename?: 'Query'; + Tweets?: Maybe }>>>; +}; + +export const LelFragmentDoc = { + kind: 'Document', + definitions: [ + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'Lel' }, + 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: 'body' } }, + ], + }, + }, + ], +} as unknown as DocumentNode; +export const FooDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'Foo' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'Tweets' }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'Field', name: { kind: 'Name', value: 'id' } }], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; +export const BarDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'Bar' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'Tweets' }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'FragmentSpread', name: { kind: 'Name', value: 'Lel' } }], + }, + }, + ], + }, + }, + ...LelFragmentDoc.definitions, + ], +} as unknown as DocumentNode; diff --git a/dev-test/gql-tag-operations-urql/gql/index.d.ts b/dev-test/gql-tag-operations-urql/gql/index.d.ts new file mode 100644 index 00000000000..985f0834eb5 --- /dev/null +++ b/dev-test/gql-tag-operations-urql/gql/index.d.ts @@ -0,0 +1,25 @@ +/* eslint-disable */ +declare module '@urql/core' { + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + + export function gql( + source: '\n query Foo {\n Tweets {\n id\n }\n }\n' + ): typeof import('./graphql').FooDocument; + + export function gql( + source: '\n fragment Lel on Tweet {\n id\n body\n }\n' + ): typeof import('./graphql').LelFragmentDoc; + + export function gql( + source: '\n query Bar {\n Tweets {\n ...Lel\n }\n }\n' + ): typeof import('./graphql').BarDocument; + + export function gql(source: string): unknown; + + export type DocumentType> = TDocumentNode extends DocumentNode< + infer TType, + any + > + ? TType + : never; +} diff --git a/dev-test/gql-tag-operations-urql/schema.graphql b/dev-test/gql-tag-operations-urql/schema.graphql new file mode 100644 index 00000000000..fe7ab4d8c8f --- /dev/null +++ b/dev-test/gql-tag-operations-urql/schema.graphql @@ -0,0 +1,52 @@ +scalar Url +scalar Date + +type Tweet { + id: ID! + body: String + date: Date + Author: User + Stats: Stat +} + +type User { + id: ID! + username: String + first_name: String + last_name: String + full_name: String + name: String @deprecated + avatar_url: Url +} + +type Stat { + views: Int + likes: Int + retweets: Int + responses: Int +} + +type Notification { + id: ID + date: Date + type: String +} + +type Meta { + count: Int +} + +type Query { + Tweet(id: ID!): Tweet + Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet] + TweetsMeta: Meta + User(id: ID!): User + Notifications(limit: Int): [Notification] + NotificationsMeta: Meta +} + +type Mutation { + createTweet(body: String): Tweet + deleteTweet(id: ID!): Tweet + markTweetRead(id: ID!): Boolean +} diff --git a/dev-test/gql-tag-operations-urql/src/index.ts b/dev-test/gql-tag-operations-urql/src/index.ts new file mode 100644 index 00000000000..efa1372c767 --- /dev/null +++ b/dev-test/gql-tag-operations-urql/src/index.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { gql } from 'urql'; + +const FooQuery = gql(/* GraphQL */ ` + query Foo { + Tweets { + id + } + } +`); + +const LelFragment = gql(/* GraphQL */ ` + fragment Lel on Tweet { + id + body + } +`); + +const BarQuery = gql(/* GraphQL */ ` + query Bar { + Tweets { + ...Lel + } + } +`); diff --git a/packages/plugins/typescript/gql-tag-operations/src/index.ts b/packages/plugins/typescript/gql-tag-operations/src/index.ts index 230072a0f3e..df6a58da846 100644 --- a/packages/plugins/typescript/gql-tag-operations/src/index.ts +++ b/packages/plugins/typescript/gql-tag-operations/src/index.ts @@ -23,24 +23,43 @@ export type DocumentType> = TDocume export const plugin: PluginFunction<{ sourcesWithOperations: Array; -}> = (_, __, { sourcesWithOperations }) => { - if (!sourcesWithOperations) { + augmentedModuleName?: string; +}> = (_, __, config) => { + if (!config.sourcesWithOperations) { return ''; } + + if (config.augmentedModuleName == null) { + return [ + `import * as graphql from './graphql';\n`, + `import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';\n`, + `\n`, + ...getDocumentRegistryChunk(config.sourcesWithOperations), + `\n`, + ...getGqlOverloadChunk(config.sourcesWithOperations, 'lookup'), + `\n`, + `export function gql(source: string): unknown;\n`, + `export function gql(source: string) {\n`, + ` return (documents as any)[source] ?? {};\n`, + `}\n`, + documentTypePartial, + ].join(``); + } + return [ - `import * as graphql from './graphql';\n`, - `import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';\n`, - `\n`, - ...getDocumentRegistryChunk(sourcesWithOperations), - `\n`, - ...getGqlOverloadChunk(sourcesWithOperations), - `\n`, - `export function gql(source: string): unknown;\n`, - `export function gql(source: string) {\n`, - ` return (documents as any)[source] ?? {};\n`, - `}\n`, - documentTypePartial, - ].join(``); + `declare module "${config.augmentedModuleName}" {`, + [ + `import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';\n`, + `\n`, + ...getGqlOverloadChunk(config.sourcesWithOperations, 'augmented'), + `export function gql(source: string): unknown;\n`, + documentTypePartial, + ] + .map(line => (line === `\n` ? line : ` ${line}`)) + .join(`\n`), + + `}`, + ].join(`\n`); }; function getDocumentRegistryChunk(sourcesWithOperations: Array = []) { @@ -59,18 +78,20 @@ function getDocumentRegistryChunk(sourcesWithOperations: Array) { +type Mode = 'lookup' | 'augmented'; + +function getGqlOverloadChunk(sourcesWithOperations: Array, mode: Mode) { const lines: Array = []; // We intentionally don't use a generic, because TS // would print very long `gql` function signatures (duplicating the source). for (const { operations, ...rest } of sourcesWithOperations) { const originalString = rest.source.rawSDL!; - lines.push( - `export function gql(source: ${JSON.stringify(originalString)}): (typeof documents)[${JSON.stringify( - originalString - )}];\n` - ); + const returnType = + mode === 'lookup' + ? `(typeof documents)[${JSON.stringify(originalString)}]` + : `typeof import('./graphql').${operations[0].initialName};`; + lines.push(`export function gql(source: ${JSON.stringify(originalString)}): ${returnType};\n`); } return lines; diff --git a/packages/presets/gql-tag-operations/src/index.ts b/packages/presets/gql-tag-operations/src/index.ts index 2fbadb88197..76da856103b 100644 --- a/packages/presets/gql-tag-operations/src/index.ts +++ b/packages/presets/gql-tag-operations/src/index.ts @@ -7,7 +7,24 @@ import * as typescriptPlugin from '@graphql-codegen/typescript'; import * as gqlTagPlugin from '@graphql-codegen/gql-tag-operations'; import { processSources } from './process-sources'; -export type GqlTagConfig = {}; +export type GqlTagConfig = { + /** + * @description Instead of generating a `gql` function, this preset can also generate a d.ts that will enhance the `gql` function of your framework. + * + * E.g. `graphql-tag` or `@urql/core`. + * + * @exampleMarkdown + * ```yml + * generates: + * gql/: + * preset: gql-tag-operations-preset + * presetConfig: + * augmentedModuleName: '@urql/core' + * + * ``` + */ + augmentedModuleName?: string; +}; export const preset: Types.OutputPreset = { buildGeneratesSection: options => { @@ -36,6 +53,8 @@ export const preset: Types.OutputPreset = { { [`gen-dts`]: { sourcesWithOperations } }, ]; + const artifactFileExtension = options.presetConfig.augmentedModuleName == null ? `ts` : `d.ts`; + return [ { filename: `${options.baseOutputDir}/graphql.ts`, @@ -46,11 +65,14 @@ export const preset: Types.OutputPreset = { documents: sources, }, { - filename: `${options.baseOutputDir}/index.ts`, + filename: `${options.baseOutputDir}/index.${artifactFileExtension}`, plugins: genDtsPlugins, pluginMap, schema: options.schema, - config: options.config, + config: { + ...options.config, + augmentedModuleName: options.presetConfig.augmentedModuleName, + }, documents: sources, }, ];