diff --git a/.changeset/loud-monkeys-yawn.md b/.changeset/loud-monkeys-yawn.md new file mode 100644 index 000000000..73f59dc07 --- /dev/null +++ b/.changeset/loud-monkeys-yawn.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/typescript-react-apollo": minor +--- + +Apollo Client `useFragment` hook diff --git a/dev-test/codegen.ts b/dev-test/codegen.ts index 0f233bec6..9f7d1d616 100644 --- a/dev-test/codegen.ts +++ b/dev-test/codegen.ts @@ -44,7 +44,15 @@ const config: CodegenConfig = { './dev-test/githunt/types.reactApollo.hooks.tsx': { schema: './dev-test/githunt/schema.json', documents: './dev-test/githunt/**/*.graphql', - plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'], + plugins: [ + 'typescript', + 'typescript-operations', + { + 'typescript-react-apollo': { + withFragmentHooks: true, + }, + }, + ], }, './dev-test/githunt/types.react-query.ts': { schema: './dev-test/githunt/schema.json', diff --git a/dev-test/githunt/types.reactApollo.hooks.tsx b/dev-test/githunt/types.reactApollo.hooks.tsx index 850c805aa..5c14c62ad 100644 --- a/dev-test/githunt/types.reactApollo.hooks.tsx +++ b/dev-test/githunt/types.reactApollo.hooks.tsx @@ -389,6 +389,52 @@ export const FeedEntryFragmentDoc = gql` ${VoteButtonsFragmentDoc} ${RepoInfoFragmentDoc} `; +export function useCommentsPageCommentFragment(identifiers: F) { + return Apollo.useFragment({ + fragment: CommentsPageCommentFragmentDoc, + fragmentName: 'CommentsPageComment', + from: { + __typename: 'Comment', + ...identifiers, + }, + }); +} +export type CommentsPageCommentFragmentHookResult = ReturnType< + typeof useCommentsPageCommentFragment +>; +export function useFeedEntryFragment(identifiers: F) { + return Apollo.useFragment({ + fragment: FeedEntryFragmentDoc, + fragmentName: 'FeedEntry', + from: { + __typename: 'Entry', + ...identifiers, + }, + }); +} +export type FeedEntryFragmentHookResult = ReturnType; +export function useRepoInfoFragment(identifiers: F) { + return Apollo.useFragment({ + fragment: RepoInfoFragmentDoc, + fragmentName: 'RepoInfo', + from: { + __typename: 'Entry', + ...identifiers, + }, + }); +} +export type RepoInfoFragmentHookResult = ReturnType; +export function useVoteButtonsFragment(identifiers: F) { + return Apollo.useFragment({ + fragment: VoteButtonsFragmentDoc, + fragmentName: 'VoteButtons', + from: { + __typename: 'Entry', + ...identifiers, + }, + }); +} +export type VoteButtonsFragmentHookResult = ReturnType; export const OnCommentAddedDocument = gql` subscription onCommentAdded($repoFullName: String!) { commentAdded(repoFullName: $repoFullName) { diff --git a/packages/plugins/typescript/react-apollo/src/config.ts b/packages/plugins/typescript/react-apollo/src/config.ts index a4bfb6aad..8fdcdf0ea 100644 --- a/packages/plugins/typescript/react-apollo/src/config.ts +++ b/packages/plugins/typescript/react-apollo/src/config.ts @@ -217,6 +217,31 @@ export interface ReactApolloRawPluginConfig extends RawClientSideBasePluginConfi * ``` */ withMutationOptionsType?: boolean; + + /** + * @description Whether or not to include wrappers for Apollo's useFragment hook. + * @default false + * + * @exampleMarkdown + * ```ts filename="codegen.ts" + * import type { CodegenConfig } from '@graphql-codegen/cli'; + * + * const config: CodegenConfig = { + * // ... + * generates: { + * 'path/to/file.ts': { + * plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'], + * config: { + * withFragmentHooks: true + * }, + * }, + * }, + * }; + * export default config; + * ``` + */ + withFragmentHooks?: boolean; + /** * @description Allows you to enable/disable the generation of docblocks in generated code. * Some IDE's (like VSCode) add extra inline information with docblocks, you can disable this feature if your preferred IDE does not. diff --git a/packages/plugins/typescript/react-apollo/src/visitor.ts b/packages/plugins/typescript/react-apollo/src/visitor.ts index bfa36b2c4..7f691a7f7 100644 --- a/packages/plugins/typescript/react-apollo/src/visitor.ts +++ b/packages/plugins/typescript/react-apollo/src/visitor.ts @@ -21,6 +21,7 @@ export interface ReactApolloPluginConfig extends ClientSideBasePluginConfig { withHooks: boolean; withMutationFn: boolean; withRefetchFn: boolean; + withFragmentHooks?: boolean; apolloReactCommonImportFrom: string; apolloReactComponentsImportFrom: string; apolloReactHocImportFrom: string; @@ -62,6 +63,7 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor< withHooks: getConfigValue(rawConfig.withHooks, true), withMutationFn: getConfigValue(rawConfig.withMutationFn, true), withRefetchFn: getConfigValue(rawConfig.withRefetchFn, false), + withFragmentHooks: getConfigValue(rawConfig.withFragmentHooks, false), apolloReactCommonImportFrom: getConfigValue( rawConfig.apolloReactCommonImportFrom, rawConfig.reactApolloVersion === 2 @@ -192,10 +194,14 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor< const baseImports = super.getImports(); const hasOperations = this._collectedOperations.length > 0; - if (!hasOperations) { + if (!hasOperations && !this.config.withFragmentHooks) { return baseImports; } + if (this.config.withFragmentHooks) { + return [...baseImports, this.getApolloReactHooksImport(false), ...Array.from(this.imports)]; + } + return [...baseImports, ...Array.from(this.imports)]; } @@ -583,4 +589,58 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor< .filter(a => a) .join('\n'); } + + public get fragments(): string { + const fragments = super.fragments; + + if (this._fragments.length === 0 || !this.config.withFragmentHooks) { + return fragments; + } + + const operationType = 'Fragment'; + + const hookFns: string[] = [fragments]; + + for (const fragment of this._fragments.values()) { + if (fragment.isExternal) { + continue; + } + + const nodeName = fragment.name ?? ''; + const suffix = this._getHookSuffix(nodeName, operationType); + const fragmentName: string = + this.convertName(nodeName, { + suffix, + useTypesPrefix: false, + useTypesSuffix: false, + }) + this.config.hooksSuffix; + + const operationTypeSuffix: string = this.getOperationSuffix(fragmentName, operationType); + + const operationResultType: string = this.convertName(nodeName, { + suffix: operationTypeSuffix + this._parsedConfig.operationResultSuffix, + }); + + const IDType = this.scalars.ID ?? 'string'; + + const hook = `export function use${fragmentName}(identifiers: F) { + return ${this.getApolloReactHooksIdentifier()}.use${operationType}<${operationResultType}>({ + fragment: ${nodeName}${this.config.fragmentVariableSuffix}, + fragmentName: "${nodeName}", + from: { + __typename: "${fragment.onType}", + ...identifiers, + }, + }); +}`; + + const hookResults = [ + `export type ${fragmentName}HookResult = ReturnType;`, + ]; + + hookFns.push([hook, hookResults].join('\n')); + } + + return hookFns.join('\n'); + } } diff --git a/packages/plugins/typescript/react-apollo/tests/react-apollo.spec.ts b/packages/plugins/typescript/react-apollo/tests/react-apollo.spec.ts index 8f91e4f35..8a7cb9156 100644 --- a/packages/plugins/typescript/react-apollo/tests/react-apollo.spec.ts +++ b/packages/plugins/typescript/react-apollo/tests/react-apollo.spec.ts @@ -70,6 +70,16 @@ describe('React Apollo', () => { } `); + const fragmentDoc = parse(/* GraphQL */ ` + fragment RepositoryFields on Repository { + full_name + html_url + owner { + avatar_url + } + } + `); + const validateTypeScript = async ( output: Types.PluginOutput, testSchema: GraphQLSchema, @@ -2725,6 +2735,40 @@ export function useListenToCommentsSubscription(baseOptions?: Apollo.Subscriptio await validateTypeScript(content, schema, docs, {}); }); + it('should import fragments from near operation file for useFragment', async () => { + const config: ReactApolloRawPluginConfig = { + documentMode: DocumentMode.external, + importDocumentNodeExternallyFrom: 'near-operation-file', + withComponent: false, + withHooks: true, + withHOC: false, + withFragmentHooks: true, + }; + + const docs = [{ location: 'path/to/document.graphql', document: fragmentDoc }]; + + const content = (await plugin(schema, docs, config, { + outputFile: 'graphql.tsx', + })) as Types.ComplexPluginOutput; + + expect(content.prepend[0]).toBeSimilarStringTo(`import * as Apollo from '@apollo/client';`); + + expect(content.content).toBeSimilarStringTo(` + export function useRepositoryFieldsFragment(identifiers: F) { + return Apollo.useFragment({ + fragment: RepositoryFieldsFragmentDoc, + fragmentName: "RepositoryFields", + from: { + __typename: "Repository", + ...identifiers, + }, + }); + } + export type RepositoryFieldsFragmentHookResult = ReturnType; + `); + await validateTypeScript(content, schema, docs, {}); + }); + it('should import Operations from near operation file for useMutation', async () => { const config: ReactApolloRawPluginConfig = { documentMode: DocumentMode.external,