Skip to content

Commit 22751f3

Browse files
renovate[bot]eddeee888
authored andcommitted
Implement externalDocuments implemented
- `externalDocuments` is used for as references during code generation. Said documents are often fragments that need to be inlined to the documents types/ast. However, the process should not generate types/ast for docs in externalDocuments. - Add changeset - Update generated files
1 parent 38483a8 commit 22751f3

File tree

19 files changed

+953
-140
lines changed

19 files changed

+953
-140
lines changed

.changeset/social-worms-report.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@graphql-codegen/gql-tag-operations': minor
3+
'@graphql-codegen/visitor-plugin-common': minor
4+
'@graphql-codegen/typescript-operations': minor
5+
'@graphql-codegen/plugin-helpers': minor
6+
'@graphql-codegen/cli': minor
7+
'@graphql-codegen/client-preset': minor
8+
---
9+
10+
Add support for `externalDocuments`
11+
12+
`externalDocuments` declares GraphQL documents that will be read but will not have type files generated for them. These documents are available to plugins for type resolution (e.g. fragment types), but no output files will be generated based on them. Accepts the same formats as `documents`.
13+
14+
This config option is useful for monorepos where each project may want to generate types for its own documents, but some may need to read shared fragments from across projects.

dev-test/codegen.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,14 @@ const config: CodegenConfig = {
265265
},
266266
},
267267
},
268+
// #region externalDocuments option
269+
'./dev-test/external-documents/app/types.generated.ts': {
270+
schema: './dev-test/external-documents/schema.graphqls',
271+
documents: ['./dev-test/external-documents/app/*.graphql.ts'],
272+
externalDocuments: ['./dev-test/external-documents/lib/*.graphql.ts'],
273+
plugins: ['typescript-operations'],
274+
},
275+
// #endregion
268276
},
269277
};
270278

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* GraphQL */ `
2+
query User($id: ID!) {
3+
user(id: $id) {
4+
id
5+
...UserFragment
6+
}
7+
}
8+
`;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type UserQueryVariables = Exact<{
2+
id: Scalars['ID']['input'];
3+
}>;
4+
5+
export type UserQuery = {
6+
__typename?: 'Query';
7+
user?: { __typename?: 'User'; id: string; name: string; role: UserRole } | null;
8+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* GraphQL */ `
2+
fragment UserFragment on User {
3+
id
4+
name
5+
role
6+
}
7+
`;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type Query {
2+
user(id: ID!): User
3+
}
4+
5+
type User {
6+
id: ID!
7+
name: String!
8+
role: UserRole!
9+
}
10+
11+
enum UserRole {
12+
ADMIN
13+
CUSTOMER
14+
}

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

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
normalizeOutputParam,
1515
Types,
1616
} from '@graphql-codegen/plugin-helpers';
17-
import { NoTypeDefinitionsFound } from '@graphql-tools/load';
17+
import { NoTypeDefinitionsFound, type UnnormalizedTypeDefPointer } from '@graphql-tools/load';
1818
import { mergeTypeDefs } from '@graphql-tools/merge';
1919
import { CodegenContext, ensureContext } from './config.js';
2020
import { getDocumentTransform } from './documentTransforms.js';
@@ -86,6 +86,7 @@ export async function executeCodegen(
8686
let rootConfig: { [key: string]: any } = {};
8787
let rootSchemas: Types.Schema[];
8888
let rootDocuments: Types.OperationDocument[];
89+
let rootExternalDocuments: Types.OperationDocument[];
8990
const generates: { [filename: string]: Types.ConfiguredOutput } = {};
9091

9192
const cache = createCache();
@@ -136,6 +137,11 @@ export async function executeCodegen(
136137
/* Normalize root "documents" field */
137138
rootDocuments = normalizeInstanceOrArray<Types.OperationDocument>(config.documents);
138139

140+
/* Normalize root "externalDocuments" field */
141+
rootExternalDocuments = normalizeInstanceOrArray<Types.OperationDocument>(
142+
config.externalDocuments,
143+
);
144+
139145
/* Normalize "generators" field */
140146
const generateKeys = Object.keys(config.generates || {});
141147

@@ -228,13 +234,15 @@ export async function executeCodegen(
228234
let outputSchemaAst: GraphQLSchema;
229235
let outputSchema: DocumentNode;
230236
const outputFileTemplateConfig = outputConfig.config || {};
231-
let outputDocuments: Types.DocumentFile[] = [];
237+
const outputDocuments: Types.DocumentFile[] = [];
232238
const outputSpecificSchemas = normalizeInstanceOrArray<Types.Schema>(
233239
outputConfig.schema,
234240
);
235241
let outputSpecificDocuments = normalizeInstanceOrArray<Types.OperationDocument>(
236242
outputConfig.documents,
237243
);
244+
let outputSpecificExternalDocuments =
245+
normalizeInstanceOrArray<Types.OperationDocument>(outputConfig.externalDocuments);
238246

239247
const preset: Types.OutputPreset | null = hasPreset
240248
? typeof outputConfig.preset === 'string'
@@ -247,6 +255,10 @@ export async function executeCodegen(
247255
filename,
248256
outputSpecificDocuments,
249257
);
258+
outputSpecificExternalDocuments = await preset.prepareDocuments(
259+
filename,
260+
outputSpecificExternalDocuments,
261+
);
250262
}
251263

252264
return subTask.newListr(
@@ -308,41 +320,102 @@ export async function executeCodegen(
308320
task: wrapTask(
309321
async () => {
310322
debugLog(`[CLI] Loading Documents`);
311-
const documentPointerMap: any = {};
323+
324+
const populateDocumentPointerMap = (
325+
allDocumentsDenormalizedPointers: Types.OperationDocument[],
326+
): UnnormalizedTypeDefPointer => {
327+
const pointer: UnnormalizedTypeDefPointer = {};
328+
for (const denormalizedPtr of allDocumentsDenormalizedPointers) {
329+
if (typeof denormalizedPtr === 'string') {
330+
pointer[denormalizedPtr] = {};
331+
} else if (typeof denormalizedPtr === 'object') {
332+
Object.assign(pointer, denormalizedPtr);
333+
}
334+
}
335+
return pointer;
336+
};
337+
312338
const allDocumentsDenormalizedPointers = [
313339
...rootDocuments,
314340
...outputSpecificDocuments,
315341
];
316-
for (const denormalizedPtr of allDocumentsDenormalizedPointers) {
317-
if (typeof denormalizedPtr === 'string') {
318-
documentPointerMap[denormalizedPtr] = {};
319-
} else if (typeof denormalizedPtr === 'object') {
320-
Object.assign(documentPointerMap, denormalizedPtr);
321-
}
322-
}
342+
const documentPointerMap = populateDocumentPointerMap(
343+
allDocumentsDenormalizedPointers,
344+
);
323345

324346
const hash = JSON.stringify(documentPointerMap);
325-
const result = await cache('documents', hash, async () => {
326-
try {
327-
const documents = await context.loadDocuments(documentPointerMap);
328-
return {
329-
documents,
330-
};
331-
} catch (error: any) {
332-
if (
333-
error instanceof NoTypeDefinitionsFound &&
334-
config.ignoreNoDocuments
335-
) {
336-
return {
337-
documents: [],
338-
};
347+
const outputDocumentsStandard = await cache(
348+
'documents',
349+
hash,
350+
async (): Promise<Types.DocumentFile[]> => {
351+
try {
352+
const documents = await context.loadDocuments(
353+
documentPointerMap,
354+
'standard',
355+
);
356+
return documents;
357+
} catch (error) {
358+
if (
359+
error instanceof NoTypeDefinitionsFound &&
360+
config.ignoreNoDocuments
361+
) {
362+
return [];
363+
}
364+
throw error;
365+
}
366+
},
367+
);
368+
369+
const allExternalDocumentsDenormalizedPointers = [
370+
...rootExternalDocuments,
371+
...outputSpecificExternalDocuments,
372+
];
373+
374+
const externalDocumentsPointerMap = populateDocumentPointerMap(
375+
allExternalDocumentsDenormalizedPointers,
376+
);
377+
378+
const externalDocumentHash = JSON.stringify(externalDocumentsPointerMap);
379+
const outputExternalDocuments = await cache(
380+
'documents',
381+
externalDocumentHash,
382+
async (): Promise<Types.DocumentFile[]> => {
383+
try {
384+
const documents = await context.loadDocuments(
385+
externalDocumentsPointerMap,
386+
'external',
387+
);
388+
return documents;
389+
} catch (error) {
390+
if (
391+
error instanceof NoTypeDefinitionsFound &&
392+
config.ignoreNoDocuments
393+
) {
394+
return [];
395+
}
396+
throw error;
339397
}
398+
},
399+
);
340400

341-
throw error;
401+
/**
402+
* Merging `standard` and `external` documents here,
403+
* so they can be processed the same way,
404+
* before passed into presets and plugins
405+
*/
406+
const processedFile: Record<string, true> = {};
407+
const mergedDocuments = [
408+
...outputDocumentsStandard,
409+
...outputExternalDocuments,
410+
];
411+
for (const file of mergedDocuments) {
412+
if (processedFile[file.hash]) {
413+
continue;
342414
}
343-
});
344415

345-
outputDocuments = result.documents;
416+
outputDocuments.push(file);
417+
processedFile[file.hash] = true;
418+
}
346419
},
347420
filename,
348421
`Load GraphQL documents: ${filename}`,
@@ -437,7 +510,7 @@ export async function executeCodegen(
437510
pluginContext,
438511
profiler: context.profiler,
439512
documentTransforms,
440-
},
513+
} satisfies Types.GenerateOptions,
441514
];
442515

443516
const process = async (outputArgs: Types.GenerateOptions) => {

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createRequire } from 'module';
44
import { resolve } from 'path';
55
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
66
import { GraphQLSchema, GraphQLSchemaExtensions, print } from 'graphql';
7-
import { GraphQLConfig } from 'graphql-config';
7+
import { GraphQLConfig, type Source } from 'graphql-config';
88
import { createJiti } from 'jiti';
99
import { env } from 'string-env-interpolation';
1010
import yaml from 'yaml';
@@ -16,6 +16,7 @@ import {
1616
Profiler,
1717
Types,
1818
} from '@graphql-codegen/plugin-helpers';
19+
import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load';
1920
import { findAndLoadGraphQLConfig } from './graphql-config.js';
2021
import {
2122
defaultDocumentsLoadOptions,
@@ -473,18 +474,22 @@ export class CodegenContext {
473474
return addHashToSchema(loadSchema(pointer, config));
474475
}
475476

476-
async loadDocuments(pointer: Types.OperationDocument[]): Promise<Types.DocumentFile[]> {
477+
async loadDocuments(
478+
pointer: UnnormalizedTypeDefPointer,
479+
type: 'standard' | 'external',
480+
): Promise<Types.DocumentFile[]> {
477481
const config = this.getConfig(defaultDocumentsLoadOptions);
478482
if (this._graphqlConfig) {
479483
// TODO: pointer won't work here
480-
return addHashToDocumentFiles(
484+
return addMetadataToSources(
481485
this._graphqlConfig
482486
.getProject(this._project)
483487
.loadDocuments(pointer, { ...config, ...config.config }),
488+
type,
484489
);
485490
}
486491

487-
return addHashToDocumentFiles(loadDocuments(pointer, config));
492+
return addMetadataToSources(loadDocuments(pointer, config), type);
488493
}
489494
}
490495

@@ -511,24 +516,27 @@ function addHashToSchema(schemaPromise: Promise<GraphQLSchema>): Promise<GraphQL
511516
});
512517
}
513518

514-
function hashDocument(doc: Types.DocumentFile) {
515-
if (doc.rawSDL) {
516-
return hashContent(doc.rawSDL);
517-
}
519+
async function addMetadataToSources(
520+
documentFilesPromise: Promise<Source[]>,
521+
type: 'standard' | 'external',
522+
): Promise<Types.DocumentFile[]> {
523+
function hashDocument(doc: Source): string | null {
524+
if (doc.rawSDL) {
525+
return hashContent(doc.rawSDL);
526+
}
518527

519-
if (doc.document) {
520-
return hashContent(print(doc.document));
521-
}
528+
if (doc.document) {
529+
return hashContent(print(doc.document));
530+
}
522531

523-
return null;
524-
}
532+
return null;
533+
}
525534

526-
function addHashToDocumentFiles(
527-
documentFilesPromise: Promise<Types.DocumentFile[]>,
528-
): Promise<Types.DocumentFile[]> {
529535
return documentFilesPromise.then(documentFiles =>
530-
documentFiles.map(doc => {
536+
// Note: `doc` here is technically `Source`, but by the end of the funciton it's `Types.DocumentFile`. This re-declaration makes TypeScript happy.
537+
documentFiles.map((doc: Types.DocumentFile): Types.DocumentFile => {
531538
doc.hash = hashDocument(doc);
539+
doc.type = type;
532540

533541
return doc;
534542
}),

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { extname, join } from 'path';
22
import { GraphQLError, GraphQLSchema } from 'graphql';
3+
import type { Source } from 'graphql-config';
34
import { Types } from '@graphql-codegen/plugin-helpers';
45
import { ApolloEngineLoader } from '@graphql-tools/apollo-engine-loader';
56
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
@@ -69,7 +70,7 @@ export async function loadSchema(
6970
export async function loadDocuments(
7071
documentPointers: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[],
7172
config: Types.Config,
72-
): Promise<Types.DocumentFile[]> {
73+
): Promise<Source[]> {
7374
const loaders = [
7475
new CodeFileLoader({
7576
pluckConfig: {

0 commit comments

Comments
 (0)