diff --git a/.changeset/social-worms-report.md b/.changeset/social-worms-report.md new file mode 100644 index 00000000000..54d0ee60700 --- /dev/null +++ b/.changeset/social-worms-report.md @@ -0,0 +1,14 @@ +--- +'@graphql-codegen/gql-tag-operations': minor +'@graphql-codegen/visitor-plugin-common': minor +'@graphql-codegen/typescript-operations': minor +'@graphql-codegen/plugin-helpers': minor +'@graphql-codegen/cli': minor +'@graphql-codegen/client-preset': minor +--- + +Add support for `externalDocuments` + +`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`. + +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. diff --git a/dev-test/codegen.ts b/dev-test/codegen.ts index 608648b8520..0e8fa26cc2b 100644 --- a/dev-test/codegen.ts +++ b/dev-test/codegen.ts @@ -265,6 +265,14 @@ const config: CodegenConfig = { }, }, }, + // #region externalDocuments option + './dev-test/external-documents/app/types.generated.ts': { + schema: './dev-test/external-documents/schema.graphqls', + documents: ['./dev-test/external-documents/app/*.graphql.ts'], + externalDocuments: ['./dev-test/external-documents/lib/*.graphql.ts'], + plugins: ['typescript-operations'], + }, + // #endregion }, }; diff --git a/dev-test/external-documents/app/User.graphql.ts b/dev-test/external-documents/app/User.graphql.ts new file mode 100644 index 00000000000..12fd4c7a9c3 --- /dev/null +++ b/dev-test/external-documents/app/User.graphql.ts @@ -0,0 +1,8 @@ +/* GraphQL */ ` + query User($id: ID!) { + user(id: $id) { + id + ...UserFragment + } + } +`; diff --git a/dev-test/external-documents/app/types.generated.ts b/dev-test/external-documents/app/types.generated.ts new file mode 100644 index 00000000000..fc9a2a812b2 --- /dev/null +++ b/dev-test/external-documents/app/types.generated.ts @@ -0,0 +1,8 @@ +export type UserQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + +export type UserQuery = { + __typename?: 'Query'; + user?: { __typename?: 'User'; id: string; name: string; role: UserRole } | null; +}; diff --git a/dev-test/external-documents/lib/UserFragment.graphql.ts b/dev-test/external-documents/lib/UserFragment.graphql.ts new file mode 100644 index 00000000000..a57919b561b --- /dev/null +++ b/dev-test/external-documents/lib/UserFragment.graphql.ts @@ -0,0 +1,7 @@ +/* GraphQL */ ` + fragment UserFragment on User { + id + name + role + } +`; diff --git a/dev-test/external-documents/schema.graphqls b/dev-test/external-documents/schema.graphqls new file mode 100644 index 00000000000..e2df2efded1 --- /dev/null +++ b/dev-test/external-documents/schema.graphqls @@ -0,0 +1,14 @@ +type Query { + user(id: ID!): User +} + +type User { + id: ID! + name: String! + role: UserRole! +} + +enum UserRole { + ADMIN + CUSTOMER +} diff --git a/packages/graphql-codegen-cli/src/codegen.ts b/packages/graphql-codegen-cli/src/codegen.ts index 80921523c88..b2ae734f9b7 100644 --- a/packages/graphql-codegen-cli/src/codegen.ts +++ b/packages/graphql-codegen-cli/src/codegen.ts @@ -14,7 +14,7 @@ import { normalizeOutputParam, Types, } from '@graphql-codegen/plugin-helpers'; -import { NoTypeDefinitionsFound } from '@graphql-tools/load'; +import { NoTypeDefinitionsFound, type UnnormalizedTypeDefPointer } from '@graphql-tools/load'; import { mergeTypeDefs } from '@graphql-tools/merge'; import { CodegenContext, ensureContext } from './config.js'; import { getDocumentTransform } from './documentTransforms.js'; @@ -86,6 +86,7 @@ export async function executeCodegen( let rootConfig: { [key: string]: any } = {}; let rootSchemas: Types.Schema[]; let rootDocuments: Types.OperationDocument[]; + let rootExternalDocuments: Types.OperationDocument[]; const generates: { [filename: string]: Types.ConfiguredOutput } = {}; const cache = createCache(); @@ -136,6 +137,11 @@ export async function executeCodegen( /* Normalize root "documents" field */ rootDocuments = normalizeInstanceOrArray(config.documents); + /* Normalize root "externalDocuments" field */ + rootExternalDocuments = normalizeInstanceOrArray( + config.externalDocuments, + ); + /* Normalize "generators" field */ const generateKeys = Object.keys(config.generates || {}); @@ -228,13 +234,15 @@ export async function executeCodegen( let outputSchemaAst: GraphQLSchema; let outputSchema: DocumentNode; const outputFileTemplateConfig = outputConfig.config || {}; - let outputDocuments: Types.DocumentFile[] = []; + const outputDocuments: Types.DocumentFile[] = []; const outputSpecificSchemas = normalizeInstanceOrArray( outputConfig.schema, ); let outputSpecificDocuments = normalizeInstanceOrArray( outputConfig.documents, ); + let outputSpecificExternalDocuments = + normalizeInstanceOrArray(outputConfig.externalDocuments); const preset: Types.OutputPreset | null = hasPreset ? typeof outputConfig.preset === 'string' @@ -247,6 +255,10 @@ export async function executeCodegen( filename, outputSpecificDocuments, ); + outputSpecificExternalDocuments = await preset.prepareDocuments( + filename, + outputSpecificExternalDocuments, + ); } return subTask.newListr( @@ -308,41 +320,102 @@ export async function executeCodegen( task: wrapTask( async () => { debugLog(`[CLI] Loading Documents`); - const documentPointerMap: any = {}; + + const populateDocumentPointerMap = ( + allDocumentsDenormalizedPointers: Types.OperationDocument[], + ): UnnormalizedTypeDefPointer => { + const pointer: UnnormalizedTypeDefPointer = {}; + for (const denormalizedPtr of allDocumentsDenormalizedPointers) { + if (typeof denormalizedPtr === 'string') { + pointer[denormalizedPtr] = {}; + } else if (typeof denormalizedPtr === 'object') { + Object.assign(pointer, denormalizedPtr); + } + } + return pointer; + }; + const allDocumentsDenormalizedPointers = [ ...rootDocuments, ...outputSpecificDocuments, ]; - for (const denormalizedPtr of allDocumentsDenormalizedPointers) { - if (typeof denormalizedPtr === 'string') { - documentPointerMap[denormalizedPtr] = {}; - } else if (typeof denormalizedPtr === 'object') { - Object.assign(documentPointerMap, denormalizedPtr); - } - } + const documentPointerMap = populateDocumentPointerMap( + allDocumentsDenormalizedPointers, + ); const hash = JSON.stringify(documentPointerMap); - const result = await cache('documents', hash, async () => { - try { - const documents = await context.loadDocuments(documentPointerMap); - return { - documents, - }; - } catch (error: any) { - if ( - error instanceof NoTypeDefinitionsFound && - config.ignoreNoDocuments - ) { - return { - documents: [], - }; + const outputDocumentsStandard = await cache( + 'documents', + hash, + async (): Promise => { + try { + const documents = await context.loadDocuments( + documentPointerMap, + 'standard', + ); + return documents; + } catch (error) { + if ( + error instanceof NoTypeDefinitionsFound && + config.ignoreNoDocuments + ) { + return []; + } + throw error; + } + }, + ); + + const allExternalDocumentsDenormalizedPointers = [ + ...rootExternalDocuments, + ...outputSpecificExternalDocuments, + ]; + + const externalDocumentsPointerMap = populateDocumentPointerMap( + allExternalDocumentsDenormalizedPointers, + ); + + const externalDocumentHash = JSON.stringify(externalDocumentsPointerMap); + const outputExternalDocuments = await cache( + 'documents', + externalDocumentHash, + async (): Promise => { + try { + const documents = await context.loadDocuments( + externalDocumentsPointerMap, + 'external', + ); + return documents; + } catch (error) { + if ( + error instanceof NoTypeDefinitionsFound && + config.ignoreNoDocuments + ) { + return []; + } + throw error; } + }, + ); - throw error; + /** + * Merging `standard` and `external` documents here, + * so they can be processed the same way, + * before passed into presets and plugins + */ + const processedFile: Record = {}; + const mergedDocuments = [ + ...outputDocumentsStandard, + ...outputExternalDocuments, + ]; + for (const file of mergedDocuments) { + if (processedFile[file.hash]) { + continue; } - }); - outputDocuments = result.documents; + outputDocuments.push(file); + processedFile[file.hash] = true; + } }, filename, `Load GraphQL documents: ${filename}`, @@ -437,7 +510,7 @@ export async function executeCodegen( pluginContext, profiler: context.profiler, documentTransforms, - }, + } satisfies Types.GenerateOptions, ]; const process = async (outputArgs: Types.GenerateOptions) => { diff --git a/packages/graphql-codegen-cli/src/config.ts b/packages/graphql-codegen-cli/src/config.ts index d54d7241822..e04547b1ad0 100644 --- a/packages/graphql-codegen-cli/src/config.ts +++ b/packages/graphql-codegen-cli/src/config.ts @@ -4,7 +4,7 @@ import { createRequire } from 'module'; import { resolve } from 'path'; import { cosmiconfig, defaultLoaders } from 'cosmiconfig'; import { GraphQLSchema, GraphQLSchemaExtensions, print } from 'graphql'; -import { GraphQLConfig } from 'graphql-config'; +import { GraphQLConfig, type Source } from 'graphql-config'; import { createJiti } from 'jiti'; import { env } from 'string-env-interpolation'; import yaml from 'yaml'; @@ -16,6 +16,7 @@ import { Profiler, Types, } from '@graphql-codegen/plugin-helpers'; +import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load'; import { findAndLoadGraphQLConfig } from './graphql-config.js'; import { defaultDocumentsLoadOptions, @@ -473,18 +474,22 @@ export class CodegenContext { return addHashToSchema(loadSchema(pointer, config)); } - async loadDocuments(pointer: Types.OperationDocument[]): Promise { + async loadDocuments( + pointer: UnnormalizedTypeDefPointer, + type: 'standard' | 'external', + ): Promise { const config = this.getConfig(defaultDocumentsLoadOptions); if (this._graphqlConfig) { // TODO: pointer won't work here - return addHashToDocumentFiles( + return addMetadataToSources( this._graphqlConfig .getProject(this._project) .loadDocuments(pointer, { ...config, ...config.config }), + type, ); } - return addHashToDocumentFiles(loadDocuments(pointer, config)); + return addMetadataToSources(loadDocuments(pointer, config), type); } } @@ -511,24 +516,27 @@ function addHashToSchema(schemaPromise: Promise): Promise, + type: 'standard' | 'external', +): Promise { + function hashDocument(doc: Source): string | null { + if (doc.rawSDL) { + return hashContent(doc.rawSDL); + } - if (doc.document) { - return hashContent(print(doc.document)); - } + if (doc.document) { + return hashContent(print(doc.document)); + } - return null; -} + return null; + } -function addHashToDocumentFiles( - documentFilesPromise: Promise, -): Promise { return documentFilesPromise.then(documentFiles => - documentFiles.map(doc => { + // Note: `doc` here is technically `Source`, but by the end of the funciton it's `Types.DocumentFile`. This re-declaration makes TypeScript happy. + documentFiles.map((doc: Types.DocumentFile): Types.DocumentFile => { doc.hash = hashDocument(doc); + doc.type = type; return doc; }), diff --git a/packages/graphql-codegen-cli/src/load.ts b/packages/graphql-codegen-cli/src/load.ts index 60fa252df0c..a03e5aa2f7f 100644 --- a/packages/graphql-codegen-cli/src/load.ts +++ b/packages/graphql-codegen-cli/src/load.ts @@ -1,5 +1,6 @@ import { extname, join } from 'path'; import { GraphQLError, GraphQLSchema } from 'graphql'; +import type { Source } from 'graphql-config'; import { Types } from '@graphql-codegen/plugin-helpers'; import { ApolloEngineLoader } from '@graphql-tools/apollo-engine-loader'; import { CodeFileLoader } from '@graphql-tools/code-file-loader'; @@ -69,7 +70,7 @@ export async function loadSchema( export async function loadDocuments( documentPointers: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[], config: Types.Config, -): Promise { +): Promise { const loaders = [ new CodeFileLoader({ pluckConfig: { diff --git a/packages/graphql-codegen-cli/tests/codegen.spec.ts b/packages/graphql-codegen-cli/tests/codegen.spec.ts index 8cd5e1e0daf..8a692a9fb2a 100644 --- a/packages/graphql-codegen-cli/tests/codegen.spec.ts +++ b/packages/graphql-codegen-cli/tests/codegen.spec.ts @@ -1298,7 +1298,7 @@ describe('Codegen Executor', () => { describe('Document Transform', () => { it('Should transform documents', async () => { const transform: Types.DocumentTransformFunction = ({ documents }) => { - const newDocuments = [ + const newDocuments: Types.DocumentFile[] = [ { document: { ...documents[0].document, @@ -1335,7 +1335,7 @@ describe('Codegen Executor', () => { }) => Types.DocumentTransformObject = ({ queryName }) => { return { transform: ({ documents }) => { - const newDocuments = [ + const newDocuments: Types.DocumentFile[] = [ { document: { ...documents[0].document, @@ -1455,7 +1455,7 @@ describe('Codegen Executor', () => { it('Should transform documents with client-preset', async () => { const transform: Types.DocumentTransformFunction = ({ documents }) => { - const newDocuments = [ + const newDocuments: Types.DocumentFile[] = [ { document: { ...documents[0].document, @@ -1487,6 +1487,167 @@ describe('Codegen Executor', () => { }); }); + describe('externalDocuments', () => { + it('should pass externalDocuments to preset buildGeneratesSection', async () => { + let capturedExternalDocuments: Types.DocumentFile[] | undefined; + + const capturePreset: Types.OutputPreset = { + buildGeneratesSection: options => { + capturedExternalDocuments = options.documents.filter(d => d.type === 'external'); + return [ + { + filename: 'out1/result.ts', + pluginMap: { typescript: require('@graphql-codegen/typescript') }, + plugins: [{ typescript: {} }], + schema: options.schema, + documents: options.documents, + config: options.config, + }, + ]; + }, + }; + + await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query root { f }`, + externalDocuments: `fragment Frag on MyType { f }`, + generates: { + 'out1/': { preset: capturePreset }, + }, + }); + + expect(capturedExternalDocuments).toBeDefined(); + expect(capturedExternalDocuments).toHaveLength(1); + }); + + it('should not include externalDocuments content in regular documents', async () => { + let capturedDocuments: Types.DocumentFile[] | undefined; + let capturedExternalDocuments: Types.DocumentFile[] | undefined; + + const capturePreset: Types.OutputPreset = { + buildGeneratesSection: options => { + capturedDocuments = options.documents.filter(d => d.type === 'standard'); + capturedExternalDocuments = options.documents.filter(d => d.type === 'external'); + return [ + { + filename: 'out1/result.ts', + pluginMap: { typescript: require('@graphql-codegen/typescript') }, + plugins: [{ typescript: {} }], + schema: options.schema, + documents: options.documents, + config: options.config, + }, + ]; + }, + }; + + await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query root { f }`, + externalDocuments: `query readOnlyQuery { f }`, + generates: { + 'out1/': { preset: capturePreset }, + }, + }); + + expect(capturedDocuments).toHaveLength(1); + expect(capturedExternalDocuments).toHaveLength(1); + + const documentNames = capturedDocuments.flatMap( + d => d.document?.definitions.map((def: any) => def.name?.value) ?? [], + ); + const readOnlyNames = capturedExternalDocuments.flatMap( + d => d.document?.definitions.map((def: any) => def.name?.value) ?? [], + ); + + expect(documentNames).toContain('root'); + expect(documentNames).not.toContain('readOnlyQuery'); + expect(readOnlyNames).toContain('readOnlyQuery'); + expect(readOnlyNames).not.toContain('root'); + }); + + it('should not include externalDocuments operations in non-preset plugin output', async () => { + const { result } = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query root { f }`, + externalDocuments: `query readOnlyQuery { f }`, + generates: { + 'out1.ts': { plugins: ['typescript-operations'] }, + }, + }); + + expect(result).toHaveLength(1); + // Only the regular document operation should be generated + expect(result[0].content).toContain('RootQuery'); + expect(result[0].content).not.toContain('ReadOnlyQuery'); + }); + + it('should support output-level externalDocuments', async () => { + let capturedExternalDocuments: Types.DocumentFile[] | undefined; + + const capturePreset: Types.OutputPreset = { + buildGeneratesSection: options => { + capturedExternalDocuments = options.documents.filter(d => d.type === 'external'); + return [ + { + filename: 'out1/result.ts', + pluginMap: { typescript: require('@graphql-codegen/typescript') }, + plugins: [{ typescript: {} }], + schema: options.schema, + documents: options.documents, + config: options.config, + }, + ]; + }, + }; + + await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + generates: { + 'out1/': { + preset: capturePreset, + externalDocuments: `fragment Frag on MyType { f }`, + }, + }, + }); + + expect(capturedExternalDocuments).toHaveLength(1); + }); + + it('should merge root and output-level externalDocuments', async () => { + let capturedExternalDocuments: Types.DocumentFile[] | undefined; + + const capturePreset: Types.OutputPreset = { + buildGeneratesSection: options => { + capturedExternalDocuments = options.documents.filter(d => d.type === 'external'); + return [ + { + filename: 'out1/result.ts', + pluginMap: { typescript: require('@graphql-codegen/typescript') }, + plugins: [{ typescript: {} }], + schema: options.schema, + documents: options.documents, + config: options.config, + }, + ]; + }, + }; + + await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + externalDocuments: `fragment RootFrag on MyType { f }`, + generates: { + 'out1/': { + preset: capturePreset, + externalDocuments: `fragment OutputFrag on MyType { f }`, + }, + }, + }); + + expect(capturedExternalDocuments).toHaveLength(2); + }); + }); + it('should not run out of memory when generating very complex types (issue #7720)', async () => { const { result } = await executeCodegen({ schema: ['../../dev-test/gatsby/schema.graphql'], diff --git a/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts b/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts index 42c15ad01aa..e4c6f8b0066 100644 --- a/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts +++ b/packages/plugins/other/visitor-plugin-common/src/optimize-operations.ts @@ -14,6 +14,7 @@ export function optimizeOperations( ); return newDocuments.map((document, index) => ({ + ...documents[index], location: documents[index]?.location || 'optimized by relay', document, })); diff --git a/packages/plugins/typescript/gql-tag-operations/src/index.ts b/packages/plugins/typescript/gql-tag-operations/src/index.ts index d9ecc9e3b33..d50f7a873f5 100644 --- a/packages/plugins/typescript/gql-tag-operations/src/index.ts +++ b/packages/plugins/typescript/gql-tag-operations/src/index.ts @@ -1,7 +1,7 @@ import { FragmentDefinitionNode, OperationDefinitionNode } from 'graphql'; import { normalizeImportExtension, PluginFunction } from '@graphql-codegen/plugin-helpers'; +import type { Types } from '@graphql-codegen/plugin-helpers'; import { DocumentMode } from '@graphql-codegen/visitor-plugin-common'; -import { Source } from '@graphql-tools/utils'; export type OperationOrFragment = { initialName: string; @@ -9,7 +9,7 @@ export type OperationOrFragment = { }; export type SourceWithOperations = { - source: Source; + source: Types.DocumentFile; operations: Array; }; diff --git a/packages/plugins/typescript/operations/src/index.ts b/packages/plugins/typescript/operations/src/index.ts index 34d5d0529ca..d047ea252be 100644 --- a/packages/plugins/typescript/operations/src/index.ts +++ b/packages/plugins/typescript/operations/src/index.ts @@ -1,4 +1,4 @@ -import { concatAST, FragmentDefinitionNode, GraphQLSchema, Kind } from 'graphql'; +import { concatAST, FragmentDefinitionNode, GraphQLSchema, Kind, type DocumentNode } from 'graphql'; import { oldVisit, PluginFunction, Types } from '@graphql-codegen/plugin-helpers'; import { LoadedFragment, optimizeOperations } from '@graphql-codegen/visitor-plugin-common'; import { TypeScriptDocumentsPluginConfig } from './config.js'; @@ -23,11 +23,43 @@ export const plugin: PluginFunction< includeFragments: config.flattenGeneratedTypesIncludeFragments, }) : rawDocuments; - const allAst = concatAST(documents.map(v => v.document)); + const parsedDocuments = documents.reduce<{ + all: { + documentFiles: Types.DocumentFile[]; + documentNodes: DocumentNode[]; + }; + standard: { + documentFiles: Types.DocumentFile[]; + documentNodes: DocumentNode[]; + }; + }>( + (prev, document) => { + prev.all.documentFiles.push(document); + prev.all.documentNodes.push(document.document); + + // `!document.type` case could happen in a few scenarios: + // - the plugin is programmatically triggered + // - in existing tests + if (!document.type || document.type === 'standard') { + prev.standard.documentFiles.push(document); + prev.standard.documentNodes.push(document.document); + } + + return prev; + }, + { + all: { documentFiles: [], documentNodes: [] }, + standard: { documentFiles: [], documentNodes: [] }, + }, + ); + + // For Fragment types to resolve correctly, we must get read all docs (`standard` and `external`) + // Fragment types are usually (but not always) in `external` files in certain setup, like a monorepo. + const allDocumentsAST = concatAST(parsedDocuments.all.documentNodes); const allFragments: LoadedFragment[] = [ ...( - allAst.definitions.filter( + allDocumentsAST.definitions.filter( d => d.kind === Kind.FRAGMENT_DEFINITION, ) as FragmentDefinitionNode[] ).map(fragmentDef => ({ @@ -41,7 +73,10 @@ export const plugin: PluginFunction< const visitor = new TypeScriptDocumentsVisitor(schema, config, allFragments); - const visitorResult = oldVisit(allAst, { + // We only visit `standard` documents to generate types. + // `external` documents are included as references for typechecking and completeness i.e. only used for reading purposes, no writing. + const documentsToVisitAST = concatAST(parsedDocuments.standard.documentNodes); + const visitorResult = oldVisit(documentsToVisitAST, { leave: visitor, }); @@ -50,7 +85,7 @@ export const plugin: PluginFunction< if (config.addOperationExport) { const exportConsts = []; - for (const d of allAst.definitions) { + for (const d of allDocumentsAST.definitions) { if ('name' in d) { exportConsts.push(`export declare const ${d.name.value}: import("graphql").DocumentNode;`); } diff --git a/packages/plugins/typescript/operations/tests/ts-documents.externalDocuments.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.externalDocuments.spec.ts new file mode 100644 index 00000000000..3c2139335fb --- /dev/null +++ b/packages/plugins/typescript/operations/tests/ts-documents.externalDocuments.spec.ts @@ -0,0 +1,63 @@ +import { buildSchema, parse } from 'graphql'; +import { mergeOutputs } from '@graphql-codegen/plugin-helpers'; +import { plugin } from '../src/index.js'; + +describe('TypeScript Operations Plugin - externalDocuments', () => { + it('uses external document file as reference, without generating types for it', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Query { + user(id: ID!): User + } + + type User { + id: ID! + name: String! + role: UserRole! + } + + enum UserRole { + ADMIN + CUSTOMER + } + `); + const query = parse(/* GraphQL */ ` + query User { + user(id: "100") { + id + ...UserFragment + } + } + `); + + const fragment = parse(/* GraphQL */ ` + fragment UserFragment on User { + id + name + } + `); + + const result = mergeOutputs([ + await plugin( + schema, + [ + { location: '', document: query, type: 'standard' }, + { location: '', document: fragment, type: 'external' }, + ], + {}, + { outputFile: '' }, + ), + ]); + + expect(result).toMatchInlineSnapshot(` + "export type UserQueryVariables = Exact<{ [key: string]: never; }>; + + + export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, name: string } | null }; + " + `); + + // FIXME: cannot call `validateTs` until next major version + // https://github.com/dotansimha/graphql-code-generator/pull/10496/changes + // validateTs(result, undefined, undefined, undefined, undefined, true); + }); +}); diff --git a/packages/presets/client/src/process-sources.ts b/packages/presets/client/src/process-sources.ts index 9a86af3e16f..c9b8e41b792 100644 --- a/packages/presets/client/src/process-sources.ts +++ b/packages/presets/client/src/process-sources.ts @@ -1,10 +1,13 @@ import { FragmentDefinitionNode, OperationDefinitionNode } from 'graphql'; import { OperationOrFragment, SourceWithOperations } from '@graphql-codegen/gql-tag-operations'; -import { Source } from '@graphql-tools/utils'; +import type { Types } from '@graphql-codegen/plugin-helpers'; export type BuildNameFunction = (type: OperationDefinitionNode | FragmentDefinitionNode) => string; -export function processSources(sources: Array, buildName: BuildNameFunction) { +export function processSources( + sources: Array, + buildName: BuildNameFunction, +): Array { const sourcesWithOperations: Array = []; for (const originalSource of sources) { @@ -86,7 +89,7 @@ export function processSources(sources: Array, buildName: BuildNameFunct * * @param source */ -function fixLinebreaks(source: Source) { +function fixLinebreaks(source: Types.DocumentFile) { const fixedSource = { ...source }; fixedSource.rawSDL = source.rawSDL.replace(/\r\n/g, '\n'); diff --git a/packages/utils/plugins-helpers/src/types.ts b/packages/utils/plugins-helpers/src/types.ts index f2aa040299b..f323b6a038f 100644 --- a/packages/utils/plugins-helpers/src/types.ts +++ b/packages/utils/plugins-helpers/src/types.ts @@ -34,7 +34,8 @@ export namespace Types { }; export interface DocumentFile extends Source { - hash?: string; + hash?: string | null; + type?: 'standard' | 'external'; } /* Utils */ @@ -303,6 +304,12 @@ export namespace Types { * For more details: https://graphql-code-generator.com/docs/config-reference/documents-field */ documents?: InstanceOrArray; + /** + * @description A pointer(s) to your 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`. + */ + externalDocuments?: InstanceOrArray; /** * @description A pointer(s) to your GraphQL schema. This schema will be available only for this specific `generates` record. * You can use one of the following: @@ -434,6 +441,12 @@ export namespace Types { * For more details: https://graphql-code-generator.com/docs/config-reference/documents-field */ documents?: InstanceOrArray; + /** + * @description A pointer(s) to your 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`. + */ + externalDocuments?: InstanceOrArray; /** * @type object * @additionalProperties true