Skip to content

Commit 0163d8b

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 0163d8b

19 files changed

Lines changed: 956 additions & 139 deletions

File tree

.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: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
normalizeOutputParam,
1515
Types,
1616
} from '@graphql-codegen/plugin-helpers';
17+
<<<<<<< HEAD
1718
import { NoTypeDefinitionsFound } from '@graphql-tools/load';
19+
=======
20+
import { NoTypeDefinitionsFound, type UnnormalizedTypeDefPointer } from '@graphql-tools/load';
21+
>>>>>>> 134a5443e (Implement externalDocuments implemented)
1822
import { mergeTypeDefs } from '@graphql-tools/merge';
1923
import { CodegenContext, ensureContext } from './config.js';
2024
import { getDocumentTransform } from './documentTransforms.js';
@@ -86,6 +90,7 @@ export async function executeCodegen(
8690
let rootConfig: { [key: string]: any } = {};
8791
let rootSchemas: Types.Schema[];
8892
let rootDocuments: Types.OperationDocument[];
93+
let rootExternalDocuments: Types.OperationDocument[];
8994
const generates: { [filename: string]: Types.ConfiguredOutput } = {};
9095

9196
const cache = createCache();
@@ -136,6 +141,11 @@ export async function executeCodegen(
136141
/* Normalize root "documents" field */
137142
rootDocuments = normalizeInstanceOrArray<Types.OperationDocument>(config.documents);
138143

144+
/* Normalize root "externalDocuments" field */
145+
rootExternalDocuments = normalizeInstanceOrArray<Types.OperationDocument>(
146+
config.externalDocuments,
147+
);
148+
139149
/* Normalize "generators" field */
140150
const generateKeys = Object.keys(config.generates || {});
141151

@@ -228,13 +238,15 @@ export async function executeCodegen(
228238
let outputSchemaAst: GraphQLSchema;
229239
let outputSchema: DocumentNode;
230240
const outputFileTemplateConfig = outputConfig.config || {};
231-
let outputDocuments: Types.DocumentFile[] = [];
241+
const outputDocuments: Types.DocumentFile[] = [];
232242
const outputSpecificSchemas = normalizeInstanceOrArray<Types.Schema>(
233243
outputConfig.schema,
234244
);
235245
let outputSpecificDocuments = normalizeInstanceOrArray<Types.OperationDocument>(
236246
outputConfig.documents,
237247
);
248+
let outputSpecificExternalDocuments =
249+
normalizeInstanceOrArray<Types.OperationDocument>(outputConfig.externalDocuments);
238250

239251
const preset: Types.OutputPreset | null = hasPreset
240252
? typeof outputConfig.preset === 'string'
@@ -247,6 +259,10 @@ export async function executeCodegen(
247259
filename,
248260
outputSpecificDocuments,
249261
);
262+
outputSpecificExternalDocuments = await preset.prepareDocuments(
263+
filename,
264+
outputSpecificExternalDocuments,
265+
);
250266
}
251267

252268
return subTask.newListr(
@@ -308,41 +324,102 @@ export async function executeCodegen(
308324
task: wrapTask(
309325
async () => {
310326
debugLog(`[CLI] Loading Documents`);
311-
const documentPointerMap: any = {};
327+
328+
const populateDocumentPointerMap = (
329+
allDocumentsDenormalizedPointers: Types.OperationDocument[],
330+
): UnnormalizedTypeDefPointer => {
331+
const pointer: UnnormalizedTypeDefPointer = {};
332+
for (const denormalizedPtr of allDocumentsDenormalizedPointers) {
333+
if (typeof denormalizedPtr === 'string') {
334+
pointer[denormalizedPtr] = {};
335+
} else if (typeof denormalizedPtr === 'object') {
336+
Object.assign(pointer, denormalizedPtr);
337+
}
338+
}
339+
return pointer;
340+
};
341+
312342
const allDocumentsDenormalizedPointers = [
313343
...rootDocuments,
314344
...outputSpecificDocuments,
315345
];
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-
}
346+
const documentPointerMap = populateDocumentPointerMap(
347+
allDocumentsDenormalizedPointers,
348+
);
323349

324350
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-
};
351+
const outputDocumentsStandard = await cache(
352+
'documents',
353+
hash,
354+
async (): Promise<Types.DocumentFile[]> => {
355+
try {
356+
const documents = await context.loadDocuments(
357+
documentPointerMap,
358+
'standard',
359+
);
360+
return documents;
361+
} catch (error) {
362+
if (
363+
error instanceof NoTypeDefinitionsFound &&
364+
config.ignoreNoDocuments
365+
) {
366+
return [];
367+
}
368+
throw error;
369+
}
370+
},
371+
);
372+
373+
const allExternalDocumentsDenormalizedPointers = [
374+
...rootExternalDocuments,
375+
...outputSpecificExternalDocuments,
376+
];
377+
378+
const externalDocumentsPointerMap = populateDocumentPointerMap(
379+
allExternalDocumentsDenormalizedPointers,
380+
);
381+
382+
const externalDocumentHash = JSON.stringify(externalDocumentsPointerMap);
383+
const outputExternalDocuments = await cache(
384+
'documents',
385+
externalDocumentHash,
386+
async (): Promise<Types.DocumentFile[]> => {
387+
try {
388+
const documents = await context.loadDocuments(
389+
externalDocumentsPointerMap,
390+
'external',
391+
);
392+
return documents;
393+
} catch (error) {
394+
if (
395+
error instanceof NoTypeDefinitionsFound &&
396+
config.ignoreNoDocuments
397+
) {
398+
return [];
399+
}
400+
throw error;
339401
}
402+
},
403+
);
340404

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

345-
outputDocuments = result.documents;
420+
outputDocuments.push(file);
421+
processedFile[file.hash] = true;
422+
}
346423
},
347424
filename,
348425
`Load GraphQL documents: ${filename}`,
@@ -437,7 +514,7 @@ export async function executeCodegen(
437514
pluginContext,
438515
profiler: context.profiler,
439516
documentTransforms,
440-
},
517+
} satisfies Types.GenerateOptions,
441518
];
442519

443520
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)