dotansimha / dotansimha/graphql-code-generator
near-operation-file: fragments defined in another gql tag of the same file lose their *FragmentDoc since visitor-plugin-common 2.13.8
- Dominant language
- TypeScript
- Stars
- 11.3k
- Forks
- 1.4k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 23
Description
### Describe the bug
With the `near-operation-file` preset, a fragment defined in one `gql` tag and spread by an operation in a second `gql` tag of the same source file no longer gets an exported `FragmentDoc` in the generated file. The operation document still interpolates `${FragmentDoc}`, so the generated file does not compile (`Cannot find name 'ThingFieldsFragmentDoc'`).
Bisected: passes with `@graphql-codegen/visitor-plugin-common` 2.13.7, fails with 2.13.8, all other packages held constant. Introduced by #8757, which replaced `ClientSideBaseVisitor._fragments` (an array) with `new Map(fragments.map(f => [f.name, f]))`.
### Why it happens
`near-operation-file-preset` reports every fragment a document spreads as an external fragment, including one whose generated output path is the same file. Since #6522 ("don't generate import statements for fragments declared in the same file") it deliberately skips the *import* for that case, but it still pushes the fragment to `externalFragments`. Plugins then build `[...localDefinitions (isExternal: false), ...externalFragments (isExternal: true)]`.
- Before 2.13.8 that array was fed into `dependency-graph`, whose `addNode` ignores an existing node, so the local definition (first) won and the doc was emitted.
- Since 2.13.8 the `Map` keeps the last entry, the external copy, and `get fragments()` filters out `isExternal` entries, so nothing is emitted.
#6522's description states the intended contract: the import is unnecessary "since the const is declared later on in the file". The regression test added with it (`#6520 - self-importing fragment`, fixture `issue-6520.ts`, which is exactly this shape: a fragment in one `gql` tag and a query spreading it in another) only asserts `not.toMatch('import { UserFieldsFragmentDoc }')`; it never asserts that `export const UserFieldsFragmentDoc` is present, so the 2.13.8 change passed it. Adding `expect(result[0].content).toMatch('export const UserFieldsFragmentDoc')` to that test reproduces the bug in-repo.
The visitor's own documentation for `includeExternalFragments` defines external fragments as ones "not defined in the same location as the operation definition", so a fragment emitted into the same output file should not count as external. Note that `includeExternalFragments: true` is not a workaround: it also emits docs for genuinely external (other-file) fragments next to their imports, producing `TS2440: Import declaration conflicts with local declaration`.
### Reproduction
`schema.graphql`
```graphql
type Query { thing: Thing }
type Thing { id: ID! name: String }
```
`src/Thing.ts`
```ts
import { gql } from "@apollo/client";
gql`fragment ThingFields on Thing { id name }`;
gql`query GetThing { thing { ...ThingFields } } ${ThingFields}`;
```
`codegen.ts`
```ts
export default {
schema: "schema.graphql",
documents: ["src/**/*.ts", "!src/**/*.generated.ts"],
generates: {
"src/generated/graphql.ts": { plugins: ["typescript"] },
"src/": {
preset: "near-operation-file",
presetConfig: { extension: ".generated.ts", baseTypesPath: "generated/graphql.ts", folder: "__generated__" },
plugins: ["typescript-operations", "typescript-react-apollo"],
},
},
};
```
- `@graphql-codegen/typescript-react-apollo@4.3.2` (pins visitor-plugin-common 2.13.1): `src/__generated__/Thing.generated.ts` contains `export const ThingFieldsFragmentDoc`.
- `@graphql-codegen/typescript-react-apollo@4.3.3` (pins 2.13.8), or 4.3.2 with an npm `overrides` entry forcing `@graphql-codegen/visitor-plugin-common` to 2.13.8 or later: the export is gone, the operation document still references it.
### Expected behavior
A fragment that is emitted into the same output file is local, so its `*FragmentDoc` should be generated. Either the preset should not report same-output-file fragments as external (consistent with #6522), or the visitor should prefer the local definition when deduplicating by name (restoring the pre-2.13.8 behavior).
### Environment
- `@graphql-codegen/cli` 7.3.1, `typescript-operations` 6.1.6, `typescript-react-apollo` 5.0.0, `near-operation-file-preset` 5.2.2, `visitor-plugin-common` 7.2.5 (also reproduced on cli 5.0.7 / operations 4.6.1 / preset 3.1.0 with only the visitor version changed)
- graphql 16.14.2, Node 24
We are working around it with a pnpm patch to the preset that `continue`s when `fragmentDetails.filePath === generatedFilePath`.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the near-operation-file-preset handling of externalFragments and the ClientSideBaseVisitor fragment deduplication introduced in visitor-plugin-common 2.13.8. Run the regression test using fixture issue-6520.ts, then add the assertion that UserFieldsFragmentDoc is exported. Done means same-output-file fragments retain their generated export without introducing imports or conflicts for genuinely external fragments.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100