From 99ec2d15f9d088162b003434b328b1a922d11eda Mon Sep 17 00:00:00 2001 From: Joakim Gunst Date: Tue, 12 Dec 2023 14:29:30 +0200 Subject: [PATCH] feat: Add field help texts to generated JSDoc (#293) * Add field help texts to generated JSDoc * Change @helpText tag to @summary * Pass single editor interface to appendType and render methods --- src/cf-definitions-builder.ts | 16 +++++--- src/commands/index.ts | 9 ++++- src/renderer/type/content-type-renderer.ts | 4 +- src/renderer/type/js-doc-renderer.ts | 42 +++++++++++++------- src/types.ts | 18 +++++++++ test/renderer/type/js-doc-renderer.test.ts | 45 ++++++++++++++++++++++ 6 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/cf-definitions-builder.ts b/src/cf-definitions-builder.ts index 684075b..363c513 100644 --- a/src/cf-definitions-builder.ts +++ b/src/cf-definitions-builder.ts @@ -9,7 +9,7 @@ import { } from 'ts-morph'; import { moduleName } from './module-name'; import { ContentTypeRenderer, DefaultContentTypeRenderer } from './renderer'; -import { CFContentType, WriteCallback } from './types'; +import { CFContentType, CFEditorInterface, WriteCallback } from './types'; import { flatten } from 'lodash'; export default class CFDefinitionsBuilder { @@ -36,14 +36,17 @@ export default class CFDefinitionsBuilder { } } - public appendType = (model: CFContentType): CFDefinitionsBuilder => { + public appendType = ( + model: CFContentType, + editorInterface?: CFEditorInterface, + ): CFDefinitionsBuilder => { if (model.sys.type !== 'ContentType') { throw new Error('given data is not describing a ContentType'); } const file = this.addFile(moduleName(model.sys.id)); for (const renderer of this.contentTypeRenderers) { - renderer.render(model, file); + renderer.render(model, file, editorInterface); } file.organizeImports({ @@ -53,9 +56,12 @@ export default class CFDefinitionsBuilder { return this; }; - public appendTypes = (models: CFContentType[]): CFDefinitionsBuilder => { + public appendTypes = ( + models: CFContentType[], + editorInterface?: CFEditorInterface, + ): CFDefinitionsBuilder => { for (const model of models) { - this.appendType(model); + this.appendType(model, editorInterface); } return this; diff --git a/src/commands/index.ts b/src/commands/index.ts index 4ac4802..ad4c1d8 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -15,6 +15,7 @@ import { V10ContentTypeRenderer, V10TypeGuardRenderer, } from '../renderer'; +import { CFEditorInterface } from '../types'; class ContentfulMdg extends Command { static description = 'Contentful Content Types (TS Definitions) Generator'; @@ -63,7 +64,6 @@ class ContentfulMdg extends Command { spaceId: flags.spaceId, managementToken: flags.token, environmentId: flags.environment, - skipEditorInterfaces: true, skipContent: true, skipRoles: true, skipWebhooks: true, @@ -92,9 +92,14 @@ class ContentfulMdg extends Command { renderers.push(flags.v10 ? new V10TypeGuardRenderer() : new TypeGuardRenderer()); } + const editorInterfaces = content.editorInterfaces as CFEditorInterface[] | undefined; + const builder = new CFDefinitionsBuilder(renderers); for (const model of content.contentTypes) { - builder.appendType(model); + const editorInterface = editorInterfaces?.find( + (e) => e.sys.contentType.sys.id === model.sys.id, + ); + builder.appendType(model, editorInterface); } if (flags.out) { diff --git a/src/renderer/type/content-type-renderer.ts b/src/renderer/type/content-type-renderer.ts index cbd4f76..01fb58d 100644 --- a/src/renderer/type/content-type-renderer.ts +++ b/src/renderer/type/content-type-renderer.ts @@ -1,11 +1,11 @@ import { Project, SourceFile } from 'ts-morph'; -import { CFContentType } from '../../types'; +import { CFContentType, CFEditorInterface } from '../../types'; import { RenderContext } from './create-default-context'; export interface ContentTypeRenderer { setup(project: Project): void; - render(contentType: CFContentType, file: SourceFile): void; + render(contentType: CFContentType, file: SourceFile, editorInterface?: CFEditorInterface): void; createContext(): RenderContext; diff --git a/src/renderer/type/js-doc-renderer.ts b/src/renderer/type/js-doc-renderer.ts index b51791a..0085021 100644 --- a/src/renderer/type/js-doc-renderer.ts +++ b/src/renderer/type/js-doc-renderer.ts @@ -1,7 +1,7 @@ import { ContentTypeField } from 'contentful'; import { ContentTypeProps } from 'contentful-management'; import { JSDocStructure, JSDocTagStructure, OptionalKind, SourceFile } from 'ts-morph'; -import { CFContentType } from '../../types'; +import { CFContentType, CFEditorInterface, CFEditorInterfaceControl } from '../../types'; import { BaseContentTypeRenderer } from './base-content-type-renderer'; type EntryDocsOptionsProps = { @@ -19,6 +19,7 @@ type FieldsDocsOptionsProps = { type FieldDocsOptionsProps = { readonly field: ContentTypeField; + readonly control?: CFEditorInterfaceControl; }; type SkeletonDocsOptionsProps = { @@ -98,19 +99,28 @@ export const defaultJsDocRenderOptions: Required = { }; }, - renderFieldDocs: ({ field }) => { + renderFieldDocs: ({ field, control }) => { + const tags: OptionalKind[] = [ + { + tagName: 'name', + text: field.name, + }, + { + tagName: 'localized', + text: field.localized.toString(), + }, + ]; + + if (control?.settings?.helpText) { + tags.push({ + tagName: 'summary', + text: control?.settings?.helpText, + }); + } + return { description: `Field type definition for field '${field.id}' (${field.name})`, - tags: [ - { - tagName: 'name', - text: field.name, - }, - { - tagName: 'localized', - text: field.localized.toString(), - }, - ], + tags, }; }, @@ -170,7 +180,11 @@ export class JsDocRenderer extends BaseContentTypeRenderer { }; } - public render = (contentType: CFContentType, file: SourceFile): void => { + public render = ( + contentType: CFContentType, + file: SourceFile, + editorInterface?: CFEditorInterface, + ): void => { const context = this.createContext(); const entryInterfaceName = context.moduleName(contentType.sys.id); @@ -202,11 +216,13 @@ export class JsDocRenderer extends BaseContentTypeRenderer { for (const field of fields) { const fieldName = field.getName(); const contentTypeField = contentType.fields.find((f) => f.id === fieldName); + const control = editorInterface?.controls.find((c) => c.fieldId === fieldName); if (contentTypeField) { field.addJsDoc( this.renderOptions.renderFieldDocs({ field: contentTypeField, + control, }), ); } diff --git a/src/types.ts b/src/types.ts index 384df69..230829f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,3 +10,21 @@ export type CFContentType = { }; fields: ContentTypeField[]; }; + +export type CFEditorInterface = { + sys: { + contentType: { + sys: { + id: string; + }; + }; + }; + controls: CFEditorInterfaceControl[]; +}; + +export type CFEditorInterfaceControl = { + fieldId: string; + settings?: { + helpText: string; + }; +}; diff --git a/test/renderer/type/js-doc-renderer.test.ts b/test/renderer/type/js-doc-renderer.test.ts index 7fd0124..7674691 100644 --- a/test/renderer/type/js-doc-renderer.test.ts +++ b/test/renderer/type/js-doc-renderer.test.ts @@ -1,6 +1,7 @@ import { Project, ScriptTarget, SourceFile } from 'ts-morph'; import { CFContentType, + CFEditorInterface, DefaultContentTypeRenderer, JsDocRenderer, V10ContentTypeRenderer, @@ -258,6 +259,50 @@ describe('A JSDoc content type renderer class', () => { `), ); }); + + it('renders field @summary tag', () => { + const defaultRenderer = new DefaultContentTypeRenderer(); + defaultRenderer.setup(project); + defaultRenderer.render(mockContentType, testFile); + + const docsRenderer = new JsDocRenderer(); + + const editorInterface: CFEditorInterface = { + sys: { contentType: { sys: { id: 'animal' } } }, + controls: [{ fieldId: 'bread', settings: { helpText: 'Help text for the bread field.' } }], + }; + + docsRenderer.render(mockContentType, testFile, editorInterface); + + expect('\n' + testFile.getFullText()).toEqual( + stripIndent(` + import type { Entry, EntryFields } from "contentful"; + + /** + * Fields type definition for content type 'TypeAnimal' + * @name TypeAnimalFields + * @type {TypeAnimalFields} + * @memberof TypeAnimal + */ + export interface TypeAnimalFields { + /** + * Field type definition for field 'bread' (Bread) + * @name Bread + * @localized false + * @summary Help text for the bread field. + */ + bread: EntryFields.Symbol; + } + + /** + * Entry type definition for content type 'animal' (Animal) + * @name TypeAnimal + * @type {TypeAnimal} + */ + export type TypeAnimal = Entry; + `), + ); + }); }); describe('with custom Entry Docs renderer', () => {