From 30cb26fa86e8885add3c276597e75c21a4cdbdc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Luijmes?= Date: Wed, 1 Jul 2026 13:38:27 +0200 Subject: [PATCH] feat(graphql): emit graphql descriptions as tsdoc comments Add an opt-in "emitDescriptions" option to the definitions generator that carries GraphQL schema descriptions into the generated TypeScript as TSDoc/JSDoc comments on types, fields, resolver methods, scalars, enums, enum members and unions. Defaults to false to keep existing generated output unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/graphql/lib/graphql-ast.explorer.ts | 31 +++++++++ .../lib/graphql-definitions.factory.ts | 1 + .../tests/graphql-ast.explorer.spec.ts | 65 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/packages/graphql/lib/graphql-ast.explorer.ts b/packages/graphql/lib/graphql-ast.explorer.ts index ddb52dde3..367d98057 100644 --- a/packages/graphql/lib/graphql-ast.explorer.ts +++ b/packages/graphql/lib/graphql-ast.explorer.ts @@ -14,6 +14,7 @@ import { OperationTypeDefinitionNode, ScalarTypeDefinitionNode, ScalarTypeExtensionNode, + StringValueNode, TypeNode, TypeSystemDefinitionNode, TypeSystemExtensionNode, @@ -25,6 +26,7 @@ import type { ClassDeclarationStructure, EnumDeclarationStructure, InterfaceDeclarationStructure, + JSDocStructure, MethodDeclarationStructure, MethodSignatureStructure, OptionalKind, @@ -85,6 +87,13 @@ export interface DefinitionsGeneratorOptions { */ enumsAsTypes?: boolean; + /** + * If true, GraphQL descriptions are emitted as TSDoc/JSDoc comments + * on the generated types and fields. + * @default false + */ + emitDescriptions?: boolean; + /** * If provided, specifies a function to transform type names. * @example (name) => `${name}Schema` @@ -248,6 +257,7 @@ export class GraphQLAstExplorer { isExported: true, isAbstract: isRoot && mode === 'class', kind: structureKind, + docs: this.getDescriptionDocs(item, options), properties: [], methods: [], }; @@ -338,6 +348,7 @@ export class GraphQLAstExplorer { type: this.addSymbolIfRoot(type), hasQuestionToken: !required || (item as FieldDefinitionNode).arguments?.length > 0, + docs: this.getDescriptionDocs(item, options), }; } @@ -362,6 +373,7 @@ export class GraphQLAstExplorer { isAbstract: mode === 'class', name: propertyName, returnType: `${type} | Promise<${type}>`, + docs: this.getDescriptionDocs(item, options), parameters: this.getFunctionParameters( (item as FieldDefinitionNode).arguments, options, @@ -476,6 +488,7 @@ export class GraphQLAstExplorer { name: transformedName, type: mappedTypeName ?? options.defaultScalarType ?? 'any', isExported: true, + docs: this.getDescriptionDocs(item, options), }; } @@ -498,17 +511,20 @@ export class GraphQLAstExplorer { name: transformedName, type: values.join(' | '), isExported: true, + docs: this.getDescriptionDocs(item, options), }; } const members = map(item.values, (value) => ({ name: get(value, 'name.value'), value: get(value, 'name.value'), + docs: this.getDescriptionDocs(value, options), })); return { kind: tsMorphLib.StructureKind.Enum, name: transformedName, members, isExported: true, + docs: this.getDescriptionDocs(item, options), }; } @@ -532,6 +548,7 @@ export class GraphQLAstExplorer { name: transformedName, type: types.join(' | '), isExported: true, + docs: this.getDescriptionDocs(item, options), }; } @@ -552,4 +569,18 @@ export class GraphQLAstExplorer { } return options.typeName(name); } + + private getDescriptionDocs( + item: { + readonly kind: string; + readonly description?: StringValueNode | null; + }, + options: DefinitionsGeneratorOptions, + ): OptionalKind[] | undefined { + const description = get(item, 'description.value'); + if (!options.emitDescriptions || !description) { + return undefined; + } + return [{ description }]; + } } diff --git a/packages/graphql/lib/graphql-definitions.factory.ts b/packages/graphql/lib/graphql-definitions.factory.ts index e4190181a..d9e71a073 100644 --- a/packages/graphql/lib/graphql-definitions.factory.ts +++ b/packages/graphql/lib/graphql-definitions.factory.ts @@ -39,6 +39,7 @@ export class GraphQLDefinitionsFactory { additionalHeader: options.additionalHeader, defaultTypeMapping: options.defaultTypeMapping, enumsAsTypes: options.enumsAsTypes, + emitDescriptions: options.emitDescriptions, typeName: options.typeName, }; diff --git a/packages/graphql/tests/graphql-ast.explorer.spec.ts b/packages/graphql/tests/graphql-ast.explorer.spec.ts index 12e03be26..8400f4fae 100644 --- a/packages/graphql/tests/graphql-ast.explorer.spec.ts +++ b/packages/graphql/tests/graphql-ast.explorer.spec.ts @@ -29,5 +29,70 @@ describe('GraphQLAstExplorer', () => { ).toBe(true); }); }); + + it('should not emit descriptions as TSDoc comments by default', async () => { + const astExplorer = new GraphQLAstExplorer(); + + const document = gql` + """ + A feline animal. + """ + type Cat { + """ + The name of the cat. + """ + name: String! + } + `; + + const sourceFile = await astExplorer.explore( + document, + '/dev/null', + 'interface', + {}, + ); + + expect(sourceFile.getFullText()).not.toContain('A feline animal.'); + expect(sourceFile.getFullText()).not.toContain('The name of the cat.'); + }); + + it('should emit descriptions as TSDoc comments when "emitDescriptions" is enabled', async () => { + const astExplorer = new GraphQLAstExplorer(); + + const document = gql` + """ + A feline animal. + """ + type Cat { + """ + The name of the cat. + """ + name: String! + } + + """ + The available colors. + """ + enum Color { + """ + The color red. + """ + RED + } + `; + + const sourceFile = await astExplorer.explore( + document, + '/dev/null', + 'interface', + { emitDescriptions: true }, + ); + const text = sourceFile.getFullText(); + + expect(text).toContain('/** A feline animal. */'); + expect(text).toContain('/** The name of the cat. */'); + expect(text).toContain('/** The available colors. */'); + expect(text).toContain('/** The color red. */'); + }); }); });