From 06d0f8a6e09027157ff40c7fd3c30bc709fdda18 Mon Sep 17 00:00:00 2001 From: Marco Link Date: Sat, 3 Feb 2024 17:08:32 +0100 Subject: [PATCH] feat: add response type renderer --- README.md | 37 ++++ src/commands/index.ts | 10 ++ src/renderer/type/response-type-renderer.ts | 168 ++++++++++++++++++ .../type/response-type-renderer.test.ts | 91 ++++++++++ 4 files changed, 306 insertions(+) create mode 100644 src/renderer/type/response-type-renderer.ts create mode 100644 test/renderer/type/response-type-renderer.test.ts diff --git a/README.md b/README.md index 5449836..5fa4d13 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - [Localized Renderer](#LocalizedContentTypeRenderer) - [JSDoc Renderer](#JSDocRenderer) - [Type Guard Renderer](#TypeGuardRenderer) + - [Response Type Renderer](#ResponseTypeRenderer) - [Direct Usage](#direct-usage) - [Browser Usage](#browser-usage) @@ -454,6 +455,42 @@ export function isTypeAnimal; +export type TypeAnimal = Entry; + +export type TypeAnimalWithUnresolvableLinksResponse = WithUnresolvableLinksResponse; +export type TypeAnimalWithoutLinkResolutionResponse = WithoutLinkResolutionResponse; +export type TypeAnimalWithoutUnresolvableLinksResponse = WithoutUnresolvableLinksResponse; +export type TypeAnimalWithAllLocalesResponse = WithAllLocalesResponse; +export type TypeAnimalWithAllLocalesAndWithoutLinkResolutionResponse = WithAllLocalesAndWithoutLinkResolutionResponse; +export type TypeAnimalWithAllLocalesAndWithoutUnresolvableLinksResponse = WithAllLocalesAndWithoutUnresolvableLinksResponse; +``` + # Direct Usage If you're not a CLI person, or you want to integrate it with your tooling workflow, you can also directly use the `CFDefinitionsBuilder` from `cf-definitions-builder.ts` diff --git a/src/commands/index.ts b/src/commands/index.ts index 9b1b72d..02ec555 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -15,6 +15,7 @@ import { V10ContentTypeRenderer, V10TypeGuardRenderer, } from '../renderer'; +import { ResponseTypeRenderer } from '../renderer/type/response-type-renderer'; import { CFEditorInterface } from '../types'; class ContentfulMdg extends Command { @@ -29,6 +30,7 @@ class ContentfulMdg extends Command { localized: Flags.boolean({ char: 'l', description: 'add localized types' }), jsdoc: Flags.boolean({ char: 'd', description: 'add JSDoc comments' }), typeguard: Flags.boolean({ char: 'g', description: 'add type guards' }), + response: Flags.boolean({ char: 'r', description: 'add response types' }), // remote access spaceId: Flags.string({ char: 's', description: 'space id' }), @@ -94,6 +96,14 @@ class ContentfulMdg extends Command { renderers.push(flags.v10 ? new V10TypeGuardRenderer() : new TypeGuardRenderer()); } + if (flags.response) { + if (!flags.v10) { + this.error('"--response" option is only available for contentful.js v10 types.'); + } + + renderers.push(new ResponseTypeRenderer()); + } + const editorInterfaces = content.editorInterfaces as CFEditorInterface[] | undefined; const builder = new CFDefinitionsBuilder(renderers); diff --git a/src/renderer/type/response-type-renderer.ts b/src/renderer/type/response-type-renderer.ts new file mode 100644 index 0000000..621bcd4 --- /dev/null +++ b/src/renderer/type/response-type-renderer.ts @@ -0,0 +1,168 @@ +import { Project, SourceFile } from 'ts-morph'; +import { CFContentType } from '../../types'; +import { renderTypeGeneric } from '../generic'; +import { BaseContentTypeRenderer } from './base-content-type-renderer'; + +/* + * Renders the response types for the contentful content types + * Based on https://github.com/contentful/contentful.js/issues/2138#issuecomment-1921923508 + */ + +const ChainModifiers = { + WITH_UNRESOLVABLE_LINKS: 'WithUnresolvableLinksResponse', + WITHOUT_UNRESOLVABLE_LINKS: 'WithoutUnresolvableLinksResponse', + WITHOUT_LINK_RESOLUTION: 'WithoutLinkResolutionResponse', + WITH_ALL_LOCALES: 'WithAllLocalesResponse', + WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION: 'WithAllLocalesAndWithoutLinkResolutionResponse', + WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK: + 'WithAllLocalesAndWithoutUnresolvableLinksResponse', +}; + +const LocaleWithDefaultTypeString = 'Locales extends LocaleCode = LocaleCode'; + +export class ResponseTypeRenderer extends BaseContentTypeRenderer { + public static readonly FILE_BASE_NAME = 'ResponseType'; + protected readonly files: SourceFile[]; + + constructor() { + super(); + this.files = []; + } + + public override setup(project: Project): void { + super.setup(project); + const file = project.createSourceFile(`${ResponseTypeRenderer.FILE_BASE_NAME}.ts`, undefined, { + overwrite: true, + }); + + file.addImportDeclaration({ + moduleSpecifier: 'contentful', + namedImports: ['Entry', 'EntrySkeletonType', 'FieldsType', 'LocaleCode'], + isTypeOnly: true, + }); + + file.addStatements('/* Utility types for response types */'); + + file.addTypeAlias({ + name: `${ChainModifiers.WITH_UNRESOLVABLE_LINKS}>`, + type: `Entry`, + isExported: true, + }); + + file.addTypeAlias({ + name: `${ChainModifiers.WITH_ALL_LOCALES}, ${LocaleWithDefaultTypeString}>`, + type: `Entry`, + isExported: true, + }); + + file.addTypeAlias({ + name: `${ChainModifiers.WITHOUT_LINK_RESOLUTION}>`, + type: `Entry`, + isExported: true, + }); + + file.addTypeAlias({ + name: `${ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS}>`, + type: `Entry`, + isExported: true, + }); + + file.addTypeAlias({ + name: `${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK}, ${LocaleWithDefaultTypeString}>`, + type: `Entry`, + isExported: true, + }); + + file.addTypeAlias({ + name: `${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION}, ${LocaleWithDefaultTypeString}>`, + type: `Entry`, + isExported: true, + }); + + file.formatText(); + this.files.push(file); + } + + public render = (contentType: CFContentType, file: SourceFile): void => { + const context = this.createContext(); + + const entitySkeletonName = context.moduleSkeletonName(contentType.sys.id); + const entityName = context.moduleName(contentType.sys.id); + + // file.addImportDeclaration({ + // moduleSpecifier: `./${ResponseTypeRenderer.FILE_BASE_NAME}`, + // namedImports: Object.values(ChainModifiers), + // isTypeOnly: true, + // }); + + context.imports.add({ + moduleSpecifier: `./${ResponseTypeRenderer.FILE_BASE_NAME}`, + namedImports: Object.values(ChainModifiers), + isTypeOnly: true, + }); + + // file.addImportDeclaration({ + // moduleSpecifier: `./${context.moduleName(contentType.sys.id)}`, + // namedImports: [entitySkeletonName], + // isTypeOnly: true, + // }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_UNRESOLVABLE_LINKS}`, + isExported: true, + type: renderTypeGeneric(ChainModifiers.WITH_UNRESOLVABLE_LINKS, entitySkeletonName), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITHOUT_LINK_RESOLUTION}`, + isExported: true, + type: renderTypeGeneric(ChainModifiers.WITHOUT_LINK_RESOLUTION, entitySkeletonName), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS}`, + isExported: true, + type: renderTypeGeneric(ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS, entitySkeletonName), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES}<${LocaleWithDefaultTypeString}>`, + isExported: true, + type: renderTypeGeneric(ChainModifiers.WITH_ALL_LOCALES, entitySkeletonName, 'Locales'), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION}<${LocaleWithDefaultTypeString}>`, + isExported: true, + type: renderTypeGeneric( + ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION, + entitySkeletonName, + 'Locales', + ), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK}<${LocaleWithDefaultTypeString}>`, + isExported: true, + type: renderTypeGeneric( + ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK, + entitySkeletonName, + 'Locales', + ), + }); + + file.organizeImports({ + ensureNewLineAtEndOfFile: true, + }); + + for (const structure of context.imports) { + file.addImportDeclaration(structure); + } + + file.formatText(); + }; + + public additionalFiles(): SourceFile[] { + return this.files; + } +} diff --git a/test/renderer/type/response-type-renderer.test.ts b/test/renderer/type/response-type-renderer.test.ts new file mode 100644 index 0000000..0e14add --- /dev/null +++ b/test/renderer/type/response-type-renderer.test.ts @@ -0,0 +1,91 @@ +import { Project, ScriptTarget, SourceFile } from 'ts-morph'; +import { CFContentType } from '../../../src'; +import { ResponseTypeRenderer } from '../../../src/renderer/type/response-type-renderer'; +import stripIndent = require('strip-indent'); + +describe('A response type renderer class', () => { + let project: Project; + let testFile: SourceFile; + + beforeEach(() => { + project = new Project({ + useInMemoryFileSystem: true, + compilerOptions: { + target: ScriptTarget.ES5, + declaration: true, + }, + }); + testFile = project.createSourceFile('test.ts'); + }); + + it('adds ResponseType.ts on setup', () => { + const renderer = new ResponseTypeRenderer(); + renderer.setup(project); + + const file = project.getSourceFile('ResponseType.ts'); + + // console.log(file?.getFullText()); + + expect(file?.getFullText()).toEqual( + stripIndent( + ` + import type { Entry, EntrySkeletonType, FieldsType, LocaleCode } from "contentful"; + /* Utility types for response types */ + export type WithUnresolvableLinksResponse> = Entry; + export type WithAllLocalesResponse, Locales extends LocaleCode = LocaleCode> = Entry; + export type WithoutLinkResolutionResponse> = Entry; + export type WithoutUnresolvableLinksResponse> = Entry; + export type WithAllLocalesAndWithoutUnresolvableLinksResponse, Locales extends LocaleCode = LocaleCode> = Entry; + export type WithAllLocalesAndWithoutLinkResolutionResponse, Locales extends LocaleCode = LocaleCode> = Entry; + ` + .replace(/.*/, '') + .slice(1), + ), + ); + }); + + it('adds localized-entry.ts on setup', () => { + const renderer = new ResponseTypeRenderer(); + renderer.setup(project); + + const contentType: CFContentType = { + name: 'display name', + sys: { + id: 'test', + type: 'Symbol', + }, + fields: [ + { + id: 'field_id', + name: 'field_name', + disabled: false, + localized: false, + required: true, + type: 'Symbol', + omitted: false, + validations: [], + }, + ], + }; + + renderer.render(contentType, testFile); + + expect(testFile.getFullText()).toEqual( + stripIndent( + ` + import type { WithAllLocalesAndWithoutLinkResolutionResponse, WithAllLocalesAndWithoutUnresolvableLinksResponse, WithAllLocalesResponse, WithUnresolvableLinksResponse, WithoutLinkResolutionResponse, WithoutUnresolvableLinksResponse } from "./ResponseType"; + import type { TypeTestSkeleton } from "./TypeTest"; + + export type TypeTestWithUnresolvableLinksResponse = WithUnresolvableLinksResponse; + export type TypeTestWithoutLinkResolutionResponse = WithoutLinkResolutionResponse; + export type TypeTestWithoutUnresolvableLinksResponse = WithoutUnresolvableLinksResponse; + export type TypeTestWithAllLocalesResponse = WithAllLocalesResponse; + export type TypeTestWithAllLocalesAndWithoutLinkResolutionResponse = WithAllLocalesAndWithoutLinkResolutionResponse; + export type TypeTestWithAllLocalesAndWithoutUnresolvableLinksResponse = WithAllLocalesAndWithoutUnresolvableLinksResponse; + `, + ) + .replace(/.*/, '') + .slice(1), + ); + }); +});