Skip to content

Commit be85118

Browse files
authored
Improve document dedupe logic (#10737)
* Move externalDocuments config option to another file for discoverability and isolation benefits * Add tests for dedupe cases * Ensure fileId takes into account file location * Add changeset * Rename
1 parent 67593bf commit be85118

9 files changed

Lines changed: 308 additions & 163 deletions

File tree

.changeset/every-turtles-write.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-codegen/cli': patch
3+
---
4+
5+
Fix issue where same SDL in different documents are ignored when handling documents vs
6+
externalDocuments

packages/graphql-codegen-cli/src/codegen.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,13 @@ export async function executeCodegen(
409409
...outputExternalDocuments,
410410
];
411411
for (const file of mergedDocuments) {
412-
if (processedFile[file.hash]) {
412+
const fileIdentifier = (file.location || '') + (file.hash || '');
413+
if (processedFile[fileIdentifier]) {
413414
continue;
414415
}
415416

416417
outputDocuments.push(file);
417-
processedFile[file.hash] = true;
418+
processedFile[fileIdentifier] = true;
418419
}
419420
},
420421
filename,
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import * as path from 'path';
2+
import type { Types } from '@graphql-codegen/plugin-helpers';
3+
import { executeCodegen } from '../src/index.js';
4+
5+
const SIMPLE_TEST_SCHEMA = /* GraphQL */ `
6+
type Query {
7+
user: User
8+
}
9+
type User {
10+
id: ID!
11+
name: String!
12+
}
13+
`;
14+
15+
describe('externalDocuments', () => {
16+
it('Dedupes documents by location + hash', async () => {
17+
const basePath = path.join(__dirname, 'codegen.config.externalDocuments');
18+
19+
let receivedDocuments: Types.DocumentFile[];
20+
await executeCodegen({
21+
schema: SIMPLE_TEST_SCHEMA,
22+
documents: [
23+
path.join(basePath, 'file*.graphql.ts'),
24+
path.join(basePath, 'external2.graphql.ts'),
25+
],
26+
pluginLoader: () => {
27+
return {
28+
plugin: (_schema, documents) => {
29+
receivedDocuments = documents;
30+
return { content: '' };
31+
},
32+
};
33+
},
34+
externalDocuments: [path.join(basePath, 'external1.graphql.ts')],
35+
generates: {
36+
'out1/generated.ts': {
37+
plugins: ['test'],
38+
},
39+
},
40+
});
41+
42+
expect(receivedDocuments.length).toBe(5);
43+
44+
const file1 = receivedDocuments.find(d => d.location.includes('file1.graphql.ts'));
45+
expect(file1.type).toBe('standard');
46+
expect(file1.rawSDL).toMatchInlineSnapshot(`
47+
"
48+
query Root {
49+
user {
50+
id
51+
}
52+
}
53+
"
54+
`);
55+
56+
const file2 = receivedDocuments.find(d => d.location.includes('file2.graphql.ts'));
57+
expect(file2.type).toBe('standard');
58+
expect(file2.rawSDL).toMatchInlineSnapshot(`
59+
"
60+
query User {
61+
user {
62+
...UserFragment
63+
}
64+
}
65+
"
66+
`);
67+
68+
const file3 = receivedDocuments.find(d => d.location.includes('file3.graphql.ts'));
69+
expect(file3.type).toBe('standard');
70+
expect(file3.rawSDL).toMatchInlineSnapshot(`
71+
"
72+
fragment UserFragment on User {
73+
name
74+
}
75+
"
76+
`);
77+
78+
const external1 = receivedDocuments.find(d => d.location.includes('external1.graphql.ts'));
79+
expect(external1.type).toBe('external');
80+
expect(external1.rawSDL).toMatchInlineSnapshot(`
81+
"
82+
fragment UserFragment on User {
83+
name
84+
}
85+
"
86+
`);
87+
88+
const external2 = receivedDocuments.find(d => d.location.includes('external2.graphql.ts'));
89+
expect(external2.type).toBe('standard');
90+
expect(external2.rawSDL).toMatchInlineSnapshot(`
91+
"
92+
fragment UserFragment2 on User {
93+
name
94+
}
95+
"
96+
`);
97+
});
98+
99+
it('should pass externalDocuments to preset buildGeneratesSection', async () => {
100+
let capturedExternalDocuments: Types.DocumentFile[] | undefined;
101+
102+
const capturePreset: Types.OutputPreset = {
103+
buildGeneratesSection: options => {
104+
capturedExternalDocuments = options.documents.filter(d => d.type === 'external');
105+
return [
106+
{
107+
filename: 'out1/result.ts',
108+
pluginMap: { typescript: require('@graphql-codegen/typescript') },
109+
plugins: [{ typescript: {} }],
110+
schema: options.schema,
111+
documents: options.documents,
112+
config: options.config,
113+
},
114+
];
115+
},
116+
};
117+
118+
await executeCodegen({
119+
schema: SIMPLE_TEST_SCHEMA,
120+
documents: `query root { user { id } }`,
121+
externalDocuments: `query readOnlyQuery { user { id } }`,
122+
generates: {
123+
'out1/': { preset: capturePreset },
124+
},
125+
});
126+
127+
expect(capturedExternalDocuments).toBeDefined();
128+
expect(capturedExternalDocuments).toHaveLength(1);
129+
});
130+
131+
it('should not include externalDocuments content in regular documents', async () => {
132+
let capturedDocuments: Types.DocumentFile[] | undefined;
133+
let capturedExternalDocuments: Types.DocumentFile[] | undefined;
134+
135+
const capturePreset: Types.OutputPreset = {
136+
buildGeneratesSection: options => {
137+
capturedDocuments = options.documents.filter(d => d.type === 'standard');
138+
capturedExternalDocuments = options.documents.filter(d => d.type === 'external');
139+
return [
140+
{
141+
filename: 'out1/result.ts',
142+
pluginMap: { typescript: require('@graphql-codegen/typescript') },
143+
plugins: [{ typescript: {} }],
144+
schema: options.schema,
145+
documents: options.documents,
146+
config: options.config,
147+
},
148+
];
149+
},
150+
};
151+
152+
await executeCodegen({
153+
schema: SIMPLE_TEST_SCHEMA,
154+
documents: `query root { user { id } }`,
155+
externalDocuments: `query readOnlyQuery { user { id } }`,
156+
generates: {
157+
'out1/': { preset: capturePreset },
158+
},
159+
});
160+
161+
expect(capturedDocuments).toHaveLength(1);
162+
expect(capturedExternalDocuments).toHaveLength(1);
163+
164+
const documentNames = capturedDocuments.flatMap(
165+
d => d.document?.definitions.map((def: any) => def.name?.value) ?? [],
166+
);
167+
const readOnlyNames = capturedExternalDocuments.flatMap(
168+
d => d.document?.definitions.map((def: any) => def.name?.value) ?? [],
169+
);
170+
171+
expect(documentNames).toContain('root');
172+
expect(documentNames).not.toContain('readOnlyQuery');
173+
expect(readOnlyNames).toContain('readOnlyQuery');
174+
expect(readOnlyNames).not.toContain('root');
175+
});
176+
177+
it('should not include externalDocuments operations in non-preset plugin output', async () => {
178+
const { result } = await executeCodegen({
179+
schema: SIMPLE_TEST_SCHEMA,
180+
documents: `query root { user { id } }`,
181+
externalDocuments: `query readOnlyQuery { user { id } }`,
182+
generates: {
183+
'out1.ts': { plugins: ['typescript-operations'] },
184+
},
185+
});
186+
187+
expect(result).toHaveLength(1);
188+
// Only the regular document operation should be generated
189+
expect(result[0].content).toContain('RootQuery');
190+
expect(result[0].content).not.toContain('ReadOnlyQuery');
191+
});
192+
193+
it('should support output-level externalDocuments', async () => {
194+
let capturedExternalDocuments: Types.DocumentFile[] | undefined;
195+
196+
const capturePreset: Types.OutputPreset = {
197+
buildGeneratesSection: options => {
198+
capturedExternalDocuments = options.documents.filter(d => d.type === 'external');
199+
return [
200+
{
201+
filename: 'out1/result.ts',
202+
pluginMap: { typescript: require('@graphql-codegen/typescript') },
203+
plugins: [{ typescript: {} }],
204+
schema: options.schema,
205+
documents: options.documents,
206+
config: options.config,
207+
},
208+
];
209+
},
210+
};
211+
212+
await executeCodegen({
213+
schema: SIMPLE_TEST_SCHEMA,
214+
generates: {
215+
'out1/': {
216+
preset: capturePreset,
217+
externalDocuments: `fragment Frag on User { id }`,
218+
},
219+
},
220+
});
221+
222+
expect(capturedExternalDocuments).toHaveLength(1);
223+
});
224+
225+
it('should merge root and output-level externalDocuments', async () => {
226+
let capturedExternalDocuments: Types.DocumentFile[] | undefined;
227+
228+
const capturePreset: Types.OutputPreset = {
229+
buildGeneratesSection: options => {
230+
capturedExternalDocuments = options.documents.filter(d => d.type === 'external');
231+
return [
232+
{
233+
filename: 'out1/result.ts',
234+
pluginMap: { typescript: require('@graphql-codegen/typescript') },
235+
plugins: [{ typescript: {} }],
236+
schema: options.schema,
237+
documents: options.documents,
238+
config: options.config,
239+
},
240+
];
241+
},
242+
};
243+
244+
await executeCodegen({
245+
schema: SIMPLE_TEST_SCHEMA,
246+
externalDocuments: `fragment RootFrag on User { id }`,
247+
generates: {
248+
'out1/': {
249+
preset: capturePreset,
250+
externalDocuments: `fragment OutputFrag on User { name }`,
251+
},
252+
},
253+
});
254+
255+
expect(capturedExternalDocuments).toHaveLength(2);
256+
});
257+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Same content as doc4 in file3.graphql.ts
2+
// but different file, so both exist
3+
export const external1 = /* GraphQL */ `
4+
fragment UserFragment on User {
5+
name
6+
}
7+
`;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// External but included in `documents` as well
2+
// So must be treated like a `standard` document
3+
export const external2 = /* GraphQL */ `
4+
fragment UserFragment2 on User {
5+
name
6+
}
7+
`;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const doc1 = /* GraphQL */ `
2+
query Root {
3+
user {
4+
id
5+
}
6+
}
7+
`;
8+
9+
// Duplicate of doc1, so should be deduped
10+
export const doc1a = /* GraphQL */ `
11+
query Root {
12+
user {
13+
id
14+
}
15+
}
16+
`;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const doc2 = /* GraphQL */ `
2+
query User {
3+
user {
4+
...UserFragment
5+
}
6+
}
7+
`;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const doc3 = /* GraphQL */ `
2+
fragment UserFragment on User {
3+
name
4+
}
5+
`;

0 commit comments

Comments
 (0)