From 6b6fe3cbcc7de748754703adce0f62f3e070a098 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 16 Feb 2023 12:31:59 +0100 Subject: [PATCH] fix: always inline and deduplicate fragment definitions (#8971) --- .changeset/rare-rats-rhyme.md | 5 + .changeset/rotten-lizards-bathe.md | 6 + dev-test/githunt/typed-document-nodes.ts | 192 +++++++++++++++++- .../gql/graphql.ts | 29 ++- .../gql-tag-operations-masking/gql/graphql.ts | 116 ++++++++++- .../gql-tag-operations-urql/gql/graphql.ts | 13 +- dev-test/gql-tag-operations/gql/graphql.ts | 13 +- .../gql-tag-operations/graphql/graphql.ts | 13 +- .../react/apollo-client/src/gql/graphql.ts | 15 +- .../react/babel-optimized/src/gql/graphql.ts | 15 +- .../react/graphql-request/src/gql/graphql.ts | 15 +- examples/react/nextjs-swr/gql/graphql.ts | 15 +- .../tanstack-react-query/src/gql/graphql.ts | 15 +- examples/react/urql/src/gql/graphql.ts | 15 +- .../vue/apollo-composable/src/gql/graphql.ts | 15 +- examples/vue/urql/src/gql/graphql.ts | 15 +- examples/vue/villus/src/gql/graphql.ts | 15 +- .../visitor-plugin-common/src/base-visitor.ts | 1 + .../src/client-side-base-visitor.ts | 90 ++++---- .../tests/typed-document-node.spec.ts | 159 --------------- .../client/tests/client-preset.spec.ts | 101 ++++++++- 21 files changed, 639 insertions(+), 234 deletions(-) create mode 100644 .changeset/rare-rats-rhyme.md create mode 100644 .changeset/rotten-lizards-bathe.md diff --git a/.changeset/rare-rats-rhyme.md b/.changeset/rare-rats-rhyme.md new file mode 100644 index 00000000000..ca3d1b36592 --- /dev/null +++ b/.changeset/rare-rats-rhyme.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +--- + +Always inline referenced fragments within their document. This prevents issues with duplicated fragments or missing fragments. diff --git a/.changeset/rotten-lizards-bathe.md b/.changeset/rotten-lizards-bathe.md new file mode 100644 index 00000000000..d795869c0c3 --- /dev/null +++ b/.changeset/rotten-lizards-bathe.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/client-preset': patch +'@graphql-codegen/typed-document-node': patch +--- + +Allow passing fragment documents to APIs like Apollos `readFragment` diff --git a/dev-test/githunt/typed-document-nodes.ts b/dev-test/githunt/typed-document-nodes.ts index b656cc87a99..83d0a671e0c 100644 --- a/dev-test/githunt/typed-document-nodes.ts +++ b/dev-test/githunt/typed-document-nodes.ts @@ -459,8 +459,59 @@ export const FeedEntryFragmentDoc = { ], }, }, - ...VoteButtonsFragmentDoc.definitions, - ...RepoInfoFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'VoteButtons' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Entry' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'score' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'vote' }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'Field', name: { kind: 'Name', value: 'vote_value' } }], + }, + }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'RepoInfo' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Entry' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'repository' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'description' } }, + { kind: 'Field', name: { kind: 'Name', value: 'stargazers_count' } }, + { kind: 'Field', name: { kind: 'Name', value: 'open_issues_count' } }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'postedBy' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'html_url' } }, + { kind: 'Field', name: { kind: 'Name', value: 'login' } }, + ], + }, + }, + ], + }, + }, ], } as unknown as DocumentNode; export const OnCommentAddedDocument = { @@ -629,7 +680,30 @@ export const CommentDocument = { ], }, }, - ...CommentsPageCommentFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'CommentsPageComment' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Comment' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'postedBy' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'login' } }, + { kind: 'Field', name: { kind: 'Name', value: 'html_url' } }, + ], + }, + }, + { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } }, + { kind: 'Field', name: { kind: 'Name', value: 'content' } }, + ], + }, + }, ], } as unknown as DocumentNode; export const CurrentUserForProfileDocument = { @@ -721,7 +795,92 @@ export const FeedDocument = { ], }, }, - ...FeedEntryFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'VoteButtons' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Entry' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'score' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'vote' }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'Field', name: { kind: 'Name', value: 'vote_value' } }], + }, + }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'RepoInfo' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Entry' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'repository' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'description' } }, + { kind: 'Field', name: { kind: 'Name', value: 'stargazers_count' } }, + { kind: 'Field', name: { kind: 'Name', value: 'open_issues_count' } }, + ], + }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'postedBy' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'html_url' } }, + { kind: 'Field', name: { kind: 'Name', value: 'login' } }, + ], + }, + }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FeedEntry' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Entry' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'commentCount' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'repository' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'full_name' } }, + { kind: 'Field', name: { kind: 'Name', value: 'html_url' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'owner' }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'Field', name: { kind: 'Name', value: 'avatar_url' } }], + }, + }, + ], + }, + }, + { kind: 'FragmentSpread', name: { kind: 'Name', value: 'VoteButtons' } }, + { kind: 'FragmentSpread', name: { kind: 'Name', value: 'RepoInfo' } }, + ], + }, + }, ], } as unknown as DocumentNode; export const SubmitRepositoryDocument = { @@ -806,7 +965,30 @@ export const SubmitCommentDocument = { ], }, }, - ...CommentsPageCommentFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'CommentsPageComment' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Comment' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { + kind: 'Field', + name: { kind: 'Name', value: 'postedBy' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'login' } }, + { kind: 'Field', name: { kind: 'Name', value: 'html_url' } }, + ], + }, + }, + { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } }, + { kind: 'Field', name: { kind: 'Name', value: 'content' } }, + ], + }, + }, ], } as unknown as DocumentNode; export const VoteDocument = { diff --git a/dev-test/gql-tag-operations-masking-star-wars/gql/graphql.ts b/dev-test/gql-tag-operations-masking-star-wars/gql/graphql.ts index f4c21499d32..5dd9c74a357 100644 --- a/dev-test/gql-tag-operations-masking-star-wars/gql/graphql.ts +++ b/dev-test/gql-tag-operations-masking-star-wars/gql/graphql.ts @@ -330,6 +330,33 @@ export const HeroDetailsWithFragmentDocument = { ], }, }, - ...HeroDetailsFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'HeroDetails' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Character' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { + kind: 'InlineFragment', + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Human' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'Field', name: { kind: 'Name', value: 'height' } }], + }, + }, + { + kind: 'InlineFragment', + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Droid' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [{ kind: 'Field', name: { kind: 'Name', value: 'primaryFunction' } }], + }, + }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/dev-test/gql-tag-operations-masking/gql/graphql.ts b/dev-test/gql-tag-operations-masking/gql/graphql.ts index 05bd6350120..cf80a16e14c 100644 --- a/dev-test/gql-tag-operations-masking/gql/graphql.ts +++ b/dev-test/gql-tag-operations-masking/gql/graphql.ts @@ -171,7 +171,28 @@ export const TweetFragmentFragmentDoc = { ], }, }, - ...TweetAuthorFragmentFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'TweetAuthorFragment' }, + 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: 'author' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'username' } }, + ], + }, + }, + ], + }, + }, ], } as unknown as DocumentNode; export const TweetsFragmentFragmentDoc = { @@ -198,7 +219,41 @@ export const TweetsFragmentFragmentDoc = { ], }, }, - ...TweetFragmentFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'TweetAuthorFragment' }, + 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: 'author' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'username' } }, + ], + }, + }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'TweetFragment' }, + 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' } }, + { kind: 'FragmentSpread', name: { kind: 'Name', value: 'TweetAuthorFragment' } }, + ], + }, + }, ], } as unknown as DocumentNode; export const TweetAppQueryDocument = { @@ -213,6 +268,61 @@ export const TweetAppQueryDocument = { selections: [{ kind: 'FragmentSpread', name: { kind: 'Name', value: 'TweetsFragment' } }], }, }, - ...TweetsFragmentFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'TweetAuthorFragment' }, + 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: 'author' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'username' } }, + ], + }, + }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'TweetFragment' }, + 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' } }, + { kind: 'FragmentSpread', name: { kind: 'Name', value: 'TweetAuthorFragment' } }, + ], + }, + }, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'TweetsFragment' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Query' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'Tweets' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'FragmentSpread', name: { kind: 'Name', value: 'TweetFragment' } }, + ], + }, + }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/dev-test/gql-tag-operations-urql/gql/graphql.ts b/dev-test/gql-tag-operations-urql/gql/graphql.ts index bd6d7bef1c8..71a75b5953c 100644 --- a/dev-test/gql-tag-operations-urql/gql/graphql.ts +++ b/dev-test/gql-tag-operations-urql/gql/graphql.ts @@ -179,6 +179,17 @@ export const BarDocument = { ], }, }, - ...LelFragmentDoc.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; diff --git a/dev-test/gql-tag-operations/gql/graphql.ts b/dev-test/gql-tag-operations/gql/graphql.ts index bd6d7bef1c8..71a75b5953c 100644 --- a/dev-test/gql-tag-operations/gql/graphql.ts +++ b/dev-test/gql-tag-operations/gql/graphql.ts @@ -179,6 +179,17 @@ export const BarDocument = { ], }, }, - ...LelFragmentDoc.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; diff --git a/dev-test/gql-tag-operations/graphql/graphql.ts b/dev-test/gql-tag-operations/graphql/graphql.ts index b4c3541c433..67bfe67faf7 100644 --- a/dev-test/gql-tag-operations/graphql/graphql.ts +++ b/dev-test/gql-tag-operations/graphql/graphql.ts @@ -181,6 +181,17 @@ export const BarDocument = { ], }, }, - ...LelFragmentDoc.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; diff --git a/examples/react/apollo-client/src/gql/graphql.ts b/examples/react/apollo-client/src/gql/graphql.ts index f650a93a789..9ca792fa77d 100644 --- a/examples/react/apollo-client/src/gql/graphql.ts +++ b/examples/react/apollo-client/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/react/babel-optimized/src/gql/graphql.ts b/examples/react/babel-optimized/src/gql/graphql.ts index f650a93a789..9ca792fa77d 100644 --- a/examples/react/babel-optimized/src/gql/graphql.ts +++ b/examples/react/babel-optimized/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/react/graphql-request/src/gql/graphql.ts b/examples/react/graphql-request/src/gql/graphql.ts index f650a93a789..9ca792fa77d 100644 --- a/examples/react/graphql-request/src/gql/graphql.ts +++ b/examples/react/graphql-request/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/react/nextjs-swr/gql/graphql.ts b/examples/react/nextjs-swr/gql/graphql.ts index 5fa4d83f89c..1b439fa77dd 100644 --- a/examples/react/nextjs-swr/gql/graphql.ts +++ b/examples/react/nextjs-swr/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/react/tanstack-react-query/src/gql/graphql.ts b/examples/react/tanstack-react-query/src/gql/graphql.ts index f650a93a789..9ca792fa77d 100644 --- a/examples/react/tanstack-react-query/src/gql/graphql.ts +++ b/examples/react/tanstack-react-query/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/react/urql/src/gql/graphql.ts b/examples/react/urql/src/gql/graphql.ts index f650a93a789..9ca792fa77d 100644 --- a/examples/react/urql/src/gql/graphql.ts +++ b/examples/react/urql/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/vue/apollo-composable/src/gql/graphql.ts b/examples/vue/apollo-composable/src/gql/graphql.ts index 4f67256e9bf..2f497291a3c 100644 --- a/examples/vue/apollo-composable/src/gql/graphql.ts +++ b/examples/vue/apollo-composable/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/vue/urql/src/gql/graphql.ts b/examples/vue/urql/src/gql/graphql.ts index 4f67256e9bf..2f497291a3c 100644 --- a/examples/vue/urql/src/gql/graphql.ts +++ b/examples/vue/urql/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/examples/vue/villus/src/gql/graphql.ts b/examples/vue/villus/src/gql/graphql.ts index 4f67256e9bf..2f497291a3c 100644 --- a/examples/vue/villus/src/gql/graphql.ts +++ b/examples/vue/villus/src/gql/graphql.ts @@ -1368,6 +1368,19 @@ export const AllFilmsWithVariablesQueryDocument = { ], }, }, - ...FilmItemFragmentDoc.definitions, + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'FilmItem' }, + typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Film' } }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'title' } }, + { kind: 'Field', name: { kind: 'Name', value: 'releaseDate' } }, + { kind: 'Field', name: { kind: 'Name', value: 'producers' } }, + ], + }, + }, ], } as unknown as DocumentNode; diff --git a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts index f70e6a7f49c..80d329888bb 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts @@ -345,6 +345,7 @@ export interface RawConfig { * Instead - all of them are imported to the Operation node. * @type boolean * @default false + * @deprecated This option is no longer needed. It will be removed in the next major version. */ dedupeFragments?: boolean; /** diff --git a/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts index 4d88bc0bb4e..2bd9e763016 100644 --- a/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts @@ -5,7 +5,6 @@ import autoBind from 'auto-bind'; import { pascalCase } from 'change-case-all'; import { DepGraph } from 'dependency-graph'; import { - DefinitionNode, DocumentNode, FragmentDefinitionNode, FragmentSpreadNode, @@ -353,29 +352,6 @@ export class ClientSideBaseVisitor< return documentStr; } - private _generateDocumentNodeMeta( - definitions: ReadonlyArray, - fragmentNames: Array - ): ExecutableDocumentNodeMeta | void { - // If the document does not contain any executable operation, we don't need to hash it - if (definitions.every(def => def.kind !== Kind.OPERATION_DEFINITION)) { - return undefined; - } - - const allDefinitions = [...definitions]; - - for (const fragment of fragmentNames) { - const fragmentRecord = this._fragments.get(fragment); - if (fragmentRecord) { - allDefinitions.push(fragmentRecord.node); - } - } - - const documentNode: DocumentNode = { kind: Kind.DOCUMENT, definitions: allDefinitions }; - - return this._onExecutableDocumentNode(documentNode); - } - protected _gql(node: FragmentDefinitionNode | OperationDefinitionNode): string { const includeNestedFragments = this.config.documentMode === DocumentMode.documentNode || @@ -397,49 +373,57 @@ export class ClientSideBaseVisitor< return JSON.stringify(gqlObj); } if (this.config.documentMode === DocumentMode.documentNodeImportFragments) { - let gqlObj = gqlTag([doc]); + const gqlObj = gqlTag([doc]); - if (this.config.optimizeDocumentNode) { - gqlObj = optimizeDocumentNode(gqlObj); - } + // We need to inline all fragments that are used in this document + // Otherwise we might encounter the following issues: + // 1. missing fragments + // 2. duplicated fragments - if (fragments.length > 0 && (!this.config.dedupeFragments || node.kind === 'OperationDefinition')) { - const definitions = [ - ...gqlObj.definitions.map(t => JSON.stringify(t)), - ...fragments.map(name => `...${name}.definitions`), - ].join(); + const fragmentDependencyNames = new Set( + fragmentNames.map(name => this.fragmentsGraph.dependenciesOf(name)).flatMap(item => item) + ); - let hashPropertyStr = ''; + for (const fragmentName of fragmentNames) { + fragmentDependencyNames.add(fragmentName); + } - if (this._onExecutableDocumentNode) { - const meta = this._generateDocumentNodeMeta(gqlObj.definitions, fragmentNames); - if (meta) { - hashPropertyStr = `"__meta__": ${JSON.stringify(meta)}, `; - if (this._omitDefinitions === true) { - return `{${hashPropertyStr}}`; - } - } - } + const jsonStringify = (json: unknown) => + JSON.stringify(json, (key, value) => (key === 'loc' ? undefined : value)); + + let definitions = [...gqlObj.definitions]; - return `{${hashPropertyStr}"kind":"${Kind.DOCUMENT}", "definitions":[${definitions}]}`; + for (const fragmentName of fragmentDependencyNames) { + definitions.push(this.fragmentsGraph.getNodeData(fragmentName).node); } - let meta: ExecutableDocumentNodeMeta | void; + if (this.config.optimizeDocumentNode) { + definitions = [ + ...optimizeDocumentNode({ + kind: Kind.DOCUMENT, + definitions, + }).definitions, + ]; + } - if (this._onExecutableDocumentNode) { - meta = this._generateDocumentNodeMeta(gqlObj.definitions, fragmentNames); - const metaNodePartial = { ['__meta__']: meta }; + let metaString = ''; - if (this._omitDefinitions === true) { - return JSON.stringify(metaNodePartial); - } + if (this._onExecutableDocumentNode && node.kind === Kind.OPERATION_DEFINITION) { + const meta = this._onExecutableDocumentNode({ + kind: Kind.DOCUMENT, + definitions, + }); if (meta) { - return JSON.stringify({ ...metaNodePartial, ...gqlObj }); + metaString = `"__meta__":${JSON.stringify(meta)},`; + + if (this._omitDefinitions === true) { + return `{${metaString.slice(0, -1)}}`; + } } } - return JSON.stringify(gqlObj); + return `{${metaString}"kind":"${Kind.DOCUMENT}","definitions":${jsonStringify(definitions)}}`; } if (this.config.documentMode === DocumentMode.string) { diff --git a/packages/plugins/typescript/typed-document-node/tests/typed-document-node.spec.ts b/packages/plugins/typescript/typed-document-node/tests/typed-document-node.spec.ts index 5a92fb3950a..6cf6ef5a35d 100644 --- a/packages/plugins/typescript/typed-document-node/tests/typed-document-node.spec.ts +++ b/packages/plugins/typescript/typed-document-node/tests/typed-document-node.spec.ts @@ -9,165 +9,6 @@ describe('TypedDocumentNode', () => { expect(result.prepend.length).toBe(0); }); - it('Duplicated nested fragments handle (dedupeFragments=true)', async () => { - const schema = buildSchema(/* GraphQL */ ` - schema { - query: Query - } - - type Query { - jobs: [Job!]! - } - - type Job { - id: ID! - recruiterName: String! - title: String! - } - `); - - const ast = parse(/* GraphQL */ ` - query GetJobs { - jobs { - ...DataForPageA - ...DataForPageB - ...JobSimpleRecruiterData - } - } - - fragment DataForPageA on Job { - id - ...JobSimpleRecruiterData - } - - fragment DataForPageB on Job { - title - ...JobSimpleRecruiterData - } - - fragment JobSimpleRecruiterData on Job { - recruiterName - } - `); - - const res = (await plugin( - schema, - [{ location: '', document: ast }], - { dedupeFragments: true }, - { outputFile: '' } - )) as Types.ComplexPluginOutput; - - expect((res.content.match(/JobSimpleRecruiterDataFragmentDoc.definitions/g) || []).length).toBe(1); - }); - - it('Check with nested and recursive fragments handle (dedupeFragments=true)', async () => { - const schema = buildSchema(/* GraphQL */ ` - type Query { - test: MyType - nested: MyOtherType - } - - type MyOtherType { - myType: MyType! - myOtherTypeRecursive: MyOtherType! - } - - type MyType { - foo: String! - } - `); - - const ast = parse(/* GraphQL */ ` - query test { - test { - ...MyTypeFields - nested { - myOtherTypeRecursive { - myType { - ...MyTypeFields - } - myOtherTypeRecursive { - ...MyOtherTypeRecursiveFields - } - } - myType { - ...MyTypeFields - } - } - } - } - - fragment MyOtherTypeRecursiveFields on MyOtherType { - myType { - ...MyTypeFields - } - } - - fragment MyTypeFields on MyType { - foo - } - `); - - const res = (await plugin( - schema, - [{ location: '', document: ast }], - { dedupeFragments: true }, - { outputFile: '' } - )) as Types.ComplexPluginOutput; - - expect((res.content.match(/MyTypeFieldsFragmentDoc.definitions/g) || []).length).toBe(1); - }); - - it('Ignore duplicated nested fragments handle (dedupeFragments=false)', async () => { - const schema = buildSchema(/* GraphQL */ ` - schema { - query: Query - } - - type Query { - jobs: [Job!]! - } - - type Job { - id: ID! - recruiterName: String! - title: String! - } - `); - - const ast = parse(/* GraphQL */ ` - query GetJobs { - jobs { - ...DataForPageA - ...DataForPageB - } - } - - fragment DataForPageA on Job { - id - ...JobSimpleRecruiterData - } - - fragment DataForPageB on Job { - title - ...JobSimpleRecruiterData - } - - fragment JobSimpleRecruiterData on Job { - recruiterName - } - `); - - const res = (await plugin( - schema, - [{ location: '', document: ast }], - { dedupeFragments: false }, - { outputFile: '' } - )) as Types.ComplexPluginOutput; - - expect((res.content.match(/JobSimpleRecruiterDataFragmentDoc.definitions/g) || []).length).toBe(2); - }); - describe('addTypenameToSelectionSets', () => { it('Check is add __typename to typed document', async () => { const schema = buildSchema(/* GraphQL */ ` diff --git a/packages/presets/client/tests/client-preset.spec.ts b/packages/presets/client/tests/client-preset.spec.ts index e801457d029..b8ad0960320 100644 --- a/packages/presets/client/tests/client-preset.spec.ts +++ b/packages/presets/client/tests/client-preset.spec.ts @@ -1288,7 +1288,7 @@ export * from "./gql.js";`); export type CFragment = { __typename?: 'Query', c?: string | null } & { ' $fragmentName'?: 'CFragment' }; - export const CFragmentDoc = {} as unknown as DocumentNode; + export const CFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"C"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"c"}}]}}]} as unknown as DocumentNode; export const ADocument = {"__meta__":{"hash":"b61b879c1eb0040bce65d70c8adfb1ae9360f52f"}} as unknown as DocumentNode; export const BDocument = {"__meta__":{"hash":"c3ea9f3f937d47d72c70055ea55c7cf88a35e608"}} as unknown as DocumentNode;" `); @@ -1452,4 +1452,103 @@ export * from "./gql.js";`); `); }); }); + + it('correctly handle fragment references', async () => { + const result = await executeCodegen({ + schema: /* GraphQL */ ` + type Query { + a: A! + } + + type A { + b: String! + a: A! + } + `, + documents: [ + /* GraphQL */ ` + fragment AC on A { + b + } + `, + /* GraphQL */ ` + fragment AA on A { + b + } + `, + /* GraphQL */ ` + fragment AB on A { + b + ...AC + ...AA + } + `, + /* GraphQL */ ` + query OI { + a { + ...AB + ...AC + } + } + `, + ], + generates: { + 'out1/': { + preset, + plugins: [], + }, + }, + }); + const graphqlFile = result.find(file => file.filename === 'out1/graphql.ts'); + expect(graphqlFile.content).toMatchInlineSnapshot(` + "/* eslint-disable */ + import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + 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; + }; + + export type A = { + __typename?: 'A'; + a: A; + b: Scalars['String']; + }; + + export type Query = { + __typename?: 'Query'; + a: A; + }; + + export type AbFragment = ( + { __typename?: 'A', b: string } + & { ' $fragmentRefs'?: { 'AcFragment': AcFragment;'AaFragment': AaFragment } } + ) & { ' $fragmentName'?: 'AbFragment' }; + + export type AaFragment = { __typename?: 'A', b: string } & { ' $fragmentName'?: 'AaFragment' }; + + export type OiQueryVariables = Exact<{ [key: string]: never; }>; + + + export type OiQuery = { __typename?: 'Query', a: ( + { __typename?: 'A' } + & { ' $fragmentRefs'?: { 'AbFragment': AbFragment;'AcFragment': AcFragment } } + ) }; + + export type AcFragment = { __typename?: 'A', b: string } & { ' $fragmentName'?: 'AcFragment' }; + + export const AcFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AC"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}}]} as unknown as DocumentNode; + export const AaFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AA"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}}]} as unknown as DocumentNode; + export const AbFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AB"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AC"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AA"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AC"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AA"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}}]} as unknown as DocumentNode; + export const OiDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OI"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"a"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AB"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AC"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AC"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AA"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AB"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"A"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"b"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AC"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AA"}}]}}]} as unknown as DocumentNode;" + `); + }); });