From 35b92cca138240f7f6c99f48d7d6eadb28d09f68 Mon Sep 17 00:00:00 2001 From: Akash Kumar <116457960+akashchamp@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:43:05 +0530 Subject: [PATCH] [visitor-plugin-common] fix: keep local fragment definition over its external copy (#10932) ClientSideBaseVisitor dedupes fragments by name into a Map, so when the same fragment is passed both as a local definition and as an external fragment (near-operation-file does this for fragments generated into the same file), the external copy won and the fragment document was no longer generated, while operations still interpolated it. Keep the local definition in that case, as before 2.13.8. --- .changeset/local-fragment-over-external.md | 5 +++ .../src/client-side-base-visitor.ts | 13 +++++- .../tests/client-side-base-visitor.spec.ts | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .changeset/local-fragment-over-external.md diff --git a/.changeset/local-fragment-over-external.md b/.changeset/local-fragment-over-external.md new file mode 100644 index 00000000000..9c453177fd5 --- /dev/null +++ b/.changeset/local-fragment-over-external.md @@ -0,0 +1,5 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +--- + +Keep the local definition of a fragment when the same fragment is also passed as an external fragment, so its `*FragmentDoc` is generated again. This fixes `near-operation-file` output for a fragment declared in one `gql` tag and spread by an operation in another `gql` tag of the same file. 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 c363539e966..eb28652e832 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 @@ -284,7 +284,18 @@ export class ClientSideBaseVisitor< this._documents = documents; this._onExecutableDocumentNode = (rawConfig as any).unstable_onExecutableDocumentNode; this._omitDefinitions = (rawConfig as any).unstable_omitDefinitions; - this._fragments = new Map(fragments.map(fragment => [fragment.name, fragment])); + const fragmentsByName = new Map(); + for (const fragment of fragments) { + // A fragment can be passed both as a local definition and as an external one, e.g. when + // near-operation-file reports a fragment generated into the same file as external. + // Keep the local definition so its document is still generated. + const existing = fragmentsByName.get(fragment.name); + if (existing && !existing.isExternal && fragment.isExternal) { + continue; + } + fragmentsByName.set(fragment.name, fragment); + } + this._fragments = fragmentsByName; this.fragmentsGraph = this._getFragmentsGraph(); autoBind(this); } diff --git a/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts b/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts index 1fc5f8f8e48..eb65458dd24 100644 --- a/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts +++ b/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts @@ -470,3 +470,46 @@ describe('includeExternalFragments', () => { expect(visitor.fragments).toContain('ExternalAFragment'); }); }); + +describe('fragments declared locally and passed as external', () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user: User + } + + type User { + id: ID! + name: String + } + `); + + const toLoadedFragments = (source: string, isExternal: boolean) => + parse(source) + .definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION) + .map(fragmentDef => ({ + node: fragmentDef, + name: fragmentDef.name.value, + onType: fragmentDef.typeCondition.name.value, + isExternal, + })); + + const userFields = /* GraphQL */ ` + fragment UserFields on User { + id + name + } + `; + + // near-operation-file also reports fragments generated into the same file as external, + // so the same fragment reaches the visitor once as local and once as external. + it('should generate the FragmentDoc of the local definition', () => { + const visitor = new ClientSideBaseVisitor( + schema, + [...toLoadedFragments(userFields, false), ...toLoadedFragments(userFields, true)], + {}, + {}, + ); + + expect(visitor.fragments).toContain('export const UserFieldsFragmentDoc'); + }); +});