diff --git a/README.md b/README.md index 5449836..d1c00d9 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) @@ -397,7 +398,11 @@ Adds type guard functions for every content type #### Example Usage ```typescript -import { CFDefinitionsBuilder, DefaultContentTypeRenderer, TypeGuardRenderer } from 'cf-content-types-generator'; +import { + CFDefinitionsBuilder, + DefaultContentTypeRenderer, + TypeGuardRenderer, +} from 'cf-content-types-generator'; const builder = new CFDefinitionsBuilder([ new DefaultContentTypeRenderer(), @@ -429,7 +434,11 @@ Adds type guard functions for every content type which are compatible with conte #### Example Usage ```typescript -import { CFDefinitionsBuilder, V10ContentTypeRenderer, V10TypeGuardRenderer } from 'cf-content-types-generator'; +import { + CFDefinitionsBuilder, + V10ContentTypeRenderer, + V10TypeGuardRenderer, +} from 'cf-content-types-generator'; const builder = new CFDefinitionsBuilder([ new V10ContentTypeRenderer(), @@ -440,20 +449,85 @@ const builder = new CFDefinitionsBuilder([ #### Example output ```typescript -import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from "contentful"; +import type { + ChainModifiers, + Entry, + EntryFieldTypes, + EntrySkeletonType, + LocaleCode, +} from 'contentful'; export interface TypeAnimalFields { bread?: EntryFieldTypes.Symbol; } -export type TypeAnimalSkeleton = EntrySkeletonType; -export type TypeAnimal = Entry; +export type TypeAnimalSkeleton = EntrySkeletonType; +export type TypeAnimal = Entry< + TypeAnimalSkeleton, + Modifiers, + Locales +>; -export function isTypeAnimal(entry: Entry): entry is TypeAnimal { - return entry.sys.contentType.sys.id === 'animal' +export function isTypeAnimal( + entry: Entry, +): entry is TypeAnimal { + return entry.sys.contentType.sys.id === 'animal'; } ``` +## ResponseTypeRenderer + +Adds response types for every content type which are compatible with contentful.js v10. + +#### Example Usage + +```typescript +import { + CFDefinitionsBuilder, + V10ContentTypeRenderer, + ResponseTypeRenderer, +} from 'cf-content-types-generator'; + +const builder = new CFDefinitionsBuilder([ + new V10ContentTypeRenderer(), + new ResponseTypeRenderer(), +]); +``` + +#### Example output + +```typescript +import type { + ChainModifiers, + Entry, + EntryFieldTypes, + EntrySkeletonType, + LocaleCode, +} from 'contentful'; + +export interface TypeAnimalFields { + bread?: EntryFieldTypes.Symbol; +} + +export type TypeAnimalSkeleton = EntrySkeletonType; +export type TypeAnimal = Entry< + TypeAnimalSkeleton, + Modifiers, + Locales +>; + +export type TypeAnimalWithoutLinkResolutionResponse = TypeAnimal<'WITHOUT_LINK_RESOLUTION'>; +export type TypeAnimalWithoutUnresolvableLinksResponse = TypeAnimal<'WITHOUT_UNRESOLVABLE_LINKS'>; +export type TypeAnimalWithAllLocalesResponse = + TypeAnimal<'WITH_ALL_LOCALES'>; +export type TypeAnimalWithAllLocalesAndWithoutLinkResolutionResponse< + Locales extends LocaleCode = LocaleCode, +> = TypeAnimal<'WITH_ALL_LOCALES' | 'WITHOUT_LINK_RESOLUTION', Locales>; +export type TypeAnimalWithAllLocalesAndWithoutUnresolvableLinksResponse< + Locales extends LocaleCode = LocaleCode, +> = TypeAnimal<'WITH_ALL_LOCALES' | 'WITHOUT_UNRESOLVABLE_LINKS', Locales>; +``` + # 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/bin/dev b/bin/dev index c547816..2552325 100755 --- a/bin/dev +++ b/bin/dev @@ -1,5 +1,5 @@ #!/usr/bin/env node (async () => { - const oclif = await import('@oclif/core') - await oclif.execute({type: 'cjs', development: true, dir: __dirname}) -})() \ No newline at end of file + const oclif = await import('@oclif/core'); + await oclif.execute({ type: 'cjs', development: true, dir: __dirname }); +})(); diff --git a/bin/run b/bin/run index ab63009..0eca115 100755 --- a/bin/run +++ b/bin/run @@ -1,5 +1,5 @@ #!/usr/bin/env node -const oclif = require('@oclif/core') +const oclif = require('@oclif/core'); -oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) \ No newline at end of file +oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')); 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..0ae8f6d --- /dev/null +++ b/src/renderer/type/response-type-renderer.ts @@ -0,0 +1,87 @@ +import { SourceFile } from 'ts-morph'; +import { CFContentType } from '../../types'; +import { renderTypeGeneric, renderTypeLiteral, renderTypeUnion } 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 = { + 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 render = (contentType: CFContentType, file: SourceFile): void => { + const context = this.createContext(); + + const entityName = context.moduleName(contentType.sys.id); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITHOUT_LINK_RESOLUTION}`, + isExported: true, + type: renderTypeGeneric( + entityName, + renderTypeUnion([renderTypeLiteral('WITHOUT_LINK_RESOLUTION')]), + ), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITHOUT_UNRESOLVABLE_LINKS}`, + isExported: true, + type: renderTypeGeneric( + entityName, + renderTypeUnion([renderTypeLiteral('WITHOUT_UNRESOLVABLE_LINKS')]), + ), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES}<${LocaleWithDefaultTypeString}>`, + isExported: true, + type: renderTypeGeneric( + entityName, + renderTypeUnion([renderTypeLiteral('WITH_ALL_LOCALES')]), + 'Locales', + ), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_LINK_RESOLUTION}<${LocaleWithDefaultTypeString}>`, + isExported: true, + type: renderTypeGeneric( + entityName, + renderTypeUnion([ + renderTypeLiteral('WITH_ALL_LOCALES'), + renderTypeLiteral('WITHOUT_LINK_RESOLUTION'), + ]), + 'Locales', + ), + }); + + file.addTypeAlias({ + name: `${entityName}${ChainModifiers.WITH_ALL_LOCALES_AND_WITHOUT_UNRESOLVABLE_LINK}<${LocaleWithDefaultTypeString}>`, + isExported: true, + type: renderTypeGeneric( + entityName, + renderTypeUnion([ + renderTypeLiteral('WITH_ALL_LOCALES'), + renderTypeLiteral('WITHOUT_UNRESOLVABLE_LINKS'), + ]), + 'Locales', + ), + }); + + file.organizeImports({ + ensureNewLineAtEndOfFile: true, + }); + + file.formatText(); + }; +} diff --git a/test/renderer/type/localized-content-type-renfderer.test.ts b/test/renderer/type/localized-content-type-renfderer.test.ts index f25e519..635793a 100644 --- a/test/renderer/type/localized-content-type-renfderer.test.ts +++ b/test/renderer/type/localized-content-type-renfderer.test.ts @@ -39,7 +39,8 @@ describe('A localized content type renderer class', () => { : EntryType[Key] }; ` - .replace(/.*/, '').slice(1), + .replace(/.*/, '') + .slice(1), ), ); }); 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..52aa208 --- /dev/null +++ b/test/renderer/type/response-type-renderer.test.ts @@ -0,0 +1,61 @@ +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 response types', () => { + 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( + ` + export type TypeTestWithoutLinkResolutionResponse = TypeTest<"WITHOUT_LINK_RESOLUTION">; + export type TypeTestWithoutUnresolvableLinksResponse = TypeTest<"WITHOUT_UNRESOLVABLE_LINKS">; + export type TypeTestWithAllLocalesResponse = TypeTest<"WITH_ALL_LOCALES", Locales>; + export type TypeTestWithAllLocalesAndWithoutLinkResolutionResponse = TypeTest<"WITHOUT_LINK_RESOLUTION" | "WITH_ALL_LOCALES", Locales>; + export type TypeTestWithAllLocalesAndWithoutUnresolvableLinksResponse = TypeTest<"WITHOUT_UNRESOLVABLE_LINKS" | "WITH_ALL_LOCALES", Locales>; + `, + ) + .replace(/.*/, '') + .slice(1), + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0086a96..0596b5e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "outDir": "lib", "strict": true, "target": "esnext", - "allowSyntheticDefaultImports": true, + "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], "exclude": ["**/*.test.ts", "lib", "test"]