diff --git a/src/contentful-typescript-codegen.ts b/src/contentful-typescript-codegen.ts index 9ac6840f..55f2291e 100644 --- a/src/contentful-typescript-codegen.ts +++ b/src/contentful-typescript-codegen.ts @@ -18,6 +18,7 @@ const cli = meow( and present, and does not provide types for Sys, Assets, or Rich Text. This is useful for ensuring raw Contentful responses will be compatible with your code. + --localization -l Output fields with localized values Examples $ contentful-typescript-codegen -o src/@types/generated/contentful.d.ts @@ -43,6 +44,11 @@ const cli = meow( alias: "i", required: false, }, + localization: { + type: "boolean", + alias: "l", + required: false, + }, }, }, ) @@ -59,7 +65,9 @@ async function runCodegen(outputFile: string) { if (cli.flags.fieldsOnly) { output = await renderFieldsOnly(contentTypes.items) } else { - output = await render(contentTypes.items, locales.items) + output = await render(contentTypes.items, locales.items, { + localization: cli.flags.localization, + }) } outputFileSync(outputPath, output) diff --git a/src/renderers/contentful/renderContentType.ts b/src/renderers/contentful/renderContentType.ts index 3f408833..456cf8eb 100644 --- a/src/renderers/contentful/renderContentType.ts +++ b/src/renderers/contentful/renderContentType.ts @@ -13,9 +13,9 @@ import renderObject from "./fields/renderObject" import renderRichText from "./fields/renderRichText" import renderSymbol from "./fields/renderSymbol" -export default function renderContentType(contentType: ContentType): string { +export default function renderContentType(contentType: ContentType, localization: boolean): string { const name = renderContentTypeId(contentType.sys.id) - const fields = renderContentTypeFields(contentType.fields) + const fields = renderContentTypeFields(contentType.fields, localization) const sys = renderSys(contentType.sys) return ` @@ -34,7 +34,7 @@ function descriptionComment(description: string | undefined) { return "" } -function renderContentTypeFields(fields: Field[]): string { +function renderContentTypeFields(fields: Field[], localization: boolean): string { return fields .filter(field => !field.omitted) .map(field => { @@ -52,7 +52,7 @@ function renderContentTypeFields(fields: Field[]): string { Text: renderSymbol, } - return renderField(field, functionMap[field.type](field)) + return renderField(field, functionMap[field.type](field), localization) }) .join("\n\n") } diff --git a/src/renderers/contentful/renderContentfulImports.ts b/src/renderers/contentful/renderContentfulImports.ts index d9d21cda..cf8a8b98 100644 --- a/src/renderers/contentful/renderContentfulImports.ts +++ b/src/renderers/contentful/renderContentfulImports.ts @@ -1,4 +1,13 @@ -export default function renderContentfulImports(): string { +export default function renderContentfulImports(localization: boolean = false): string { + if (localization) { + return ` + // THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + + import { Entry } from 'contentful' + import { Document } from '@contentful/rich-text-types' + ` + } + return ` // THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. diff --git a/src/renderers/contentful/renderField.ts b/src/renderers/contentful/renderField.ts index 7ee1731b..ae8bb1c8 100644 --- a/src/renderers/contentful/renderField.ts +++ b/src/renderers/contentful/renderField.ts @@ -1,6 +1,10 @@ import { Field } from "contentful" import renderInterfaceProperty from "../typescript/renderInterfaceProperty" -export default function renderField(field: Field, type: string): string { - return renderInterfaceProperty(field.id, type, field.required, field.name) +export default function renderField( + field: Field, + type: string, + localization: boolean = false, +): string { + return renderInterfaceProperty(field.id, type, field.required, localization, field.name) } diff --git a/src/renderers/contentful/renderLocalizedTypes.ts b/src/renderers/contentful/renderLocalizedTypes.ts new file mode 100644 index 00000000..a2b17066 --- /dev/null +++ b/src/renderers/contentful/renderLocalizedTypes.ts @@ -0,0 +1,30 @@ +/** renders helper types for --localization flag */ +export default function renderLocalizedTypes(localization: boolean) { + if (!localization) return null + + return ` + export type LocalizedField = Partial> + + // We have to use our own localized version of Asset because of a bug in contentful https://github.com/contentful/contentful.js/issues/208 + export interface Asset { + sys: Sys + fields: { + title: LocalizedField + description: LocalizedField + file: LocalizedField<{ + url: string + details: { + size: number + image?: { + width: number + height: number + } + } + fileName: string + contentType: string + }> + } + toPlainObject(): object + } + ` +} diff --git a/src/renderers/render.ts b/src/renderers/render.ts index 3ff3768b..65e2a962 100644 --- a/src/renderers/render.ts +++ b/src/renderers/render.ts @@ -7,25 +7,35 @@ import renderContentType from "./contentful/renderContentType" import renderUnion from "./typescript/renderUnion" import renderAllLocales from "./contentful/renderAllLocales" import renderDefaultLocale from "./contentful/renderDefaultLocale" +import renderLocalizedTypes from "./contentful/renderLocalizedTypes" -export default async function render(contentTypes: ContentType[], locales: Locale[]) { +interface Options { + localization?: boolean +} + +export default async function render( + contentTypes: ContentType[], + locales: Locale[], + { localization = false }: Options = {}, +) { const sortedContentTypes = contentTypes.sort((a, b) => a.sys.id.localeCompare(b.sys.id)) const sortedLocales = locales.sort((a, b) => a.code.localeCompare(b.code)) const source = [ - renderContentfulImports(), - renderAllContentTypes(sortedContentTypes), + renderContentfulImports(localization), + renderAllContentTypes(sortedContentTypes, localization), renderAllContentTypeIds(sortedContentTypes), renderAllLocales(sortedLocales), renderDefaultLocale(sortedLocales), + renderLocalizedTypes(localization), ].join("\n\n") const prettierConfig = await resolveConfig(process.cwd()) return format(source, { ...prettierConfig, parser: "typescript" }) } -function renderAllContentTypes(contentTypes: ContentType[]): string { - return contentTypes.map(contentType => renderContentType(contentType)).join("\n\n") +function renderAllContentTypes(contentTypes: ContentType[], localization: boolean): string { + return contentTypes.map(contentType => renderContentType(contentType, localization)).join("\n\n") } function renderAllContentTypeIds(contentTypes: ContentType[]): string { diff --git a/src/renderers/typescript/renderInterfaceProperty.ts b/src/renderers/typescript/renderInterfaceProperty.ts index 9e5c75f4..2f514049 100644 --- a/src/renderers/typescript/renderInterfaceProperty.ts +++ b/src/renderers/typescript/renderInterfaceProperty.ts @@ -2,6 +2,7 @@ export default function renderInterfaceProperty( name: string, type: string, required: boolean, + localization: boolean, description?: string, ): string { return [ @@ -9,7 +10,7 @@ export default function renderInterfaceProperty( name, required ? "" : "?", ": ", - type, + localization ? `LocalizedField<${type}>` : type, required ? "" : " | undefined", ";", ].join("") diff --git a/test/renderers/contentful/renderContentType.test.ts b/test/renderers/contentful/renderContentType.test.ts index 072645f0..af9df218 100644 --- a/test/renderers/contentful/renderContentType.test.ts +++ b/test/renderers/contentful/renderContentType.test.ts @@ -55,7 +55,7 @@ describe("renderContentType()", () => { } it("works with miscellaneous field types", () => { - expect(format(renderContentType(contentType))).toMatchInlineSnapshot(` + expect(format(renderContentType(contentType, false))).toMatchInlineSnapshot(` "export interface IMyContentTypeFields { /** Symbol Field™ */ symbolField?: string | undefined; @@ -84,7 +84,7 @@ describe("renderContentType()", () => { }) it("supports descriptions", () => { - expect(format(renderContentType(contentTypeWithDescription))).toMatchInlineSnapshot(` + expect(format(renderContentType(contentTypeWithDescription, false))).toMatchInlineSnapshot(` "export interface IMyContentTypeFields {} /** This is a description */ @@ -107,4 +107,33 @@ describe("renderContentType()", () => { }" `) }) + + it("works with localized fields", () => { + expect(format(renderContentType(contentType, true))).toMatchInlineSnapshot(` + "export interface IMyContentTypeFields { + /** Symbol Field™ */ + symbolField?: LocalizedField | undefined; + + /** Array field */ + arrayField: LocalizedField<(\\"one\\" | \\"of\\" | \\"the\\" | \\"above\\")[]>; + } + + export interface IMyContentType extends Entry { + sys: { + id: string, + type: string, + createdAt: string, + updatedAt: string, + locale: string, + contentType: { + sys: { + id: \\"myContentType\\", + linkType: \\"ContentType\\", + type: \\"Link\\" + } + } + }; + }" + `) + }) }) diff --git a/test/renderers/contentful/renderContentfulImports.test.ts b/test/renderers/contentful/renderContentfulImports.test.ts index 4f30c78e..5c8f3c87 100644 --- a/test/renderers/contentful/renderContentfulImports.test.ts +++ b/test/renderers/contentful/renderContentfulImports.test.ts @@ -4,10 +4,19 @@ import format from "../../support/format" describe("renderContentfulImports()", () => { it("renders the top of the codegen file", () => { expect(format(renderContentfulImports())).toMatchInlineSnapshot(` -"// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. -import { Asset, Entry } from \\"contentful\\"; -import { Document } from \\"@contentful/rich-text-types\\";" -`) + import { Asset, Entry } from \\"contentful\\"; + import { Document } from \\"@contentful/rich-text-types\\";" + `) + }) + + it("renders the localized top of the codegen file", () => { + expect(format(renderContentfulImports(true))).toMatchInlineSnapshot(` + "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + + import { Entry } from \\"contentful\\"; + import { Document } from \\"@contentful/rich-text-types\\";" + `) }) }) diff --git a/test/renderers/render.test.ts b/test/renderers/render.test.ts index 7ca387eb..1ade6004 100644 --- a/test/renderers/render.test.ts +++ b/test/renderers/render.test.ts @@ -2,7 +2,94 @@ import render from "../../src/renderers/render" import { ContentType, Sys, Locale } from "contentful" describe("render()", () => { - it("renders given a content type", async () => { + const contentTypes: ContentType[] = [ + { + sys: { + id: "myContentType", + } as Sys, + fields: [ + { + id: "arrayField", + name: "Array field", + required: true, + validations: [{}], + items: { + type: "Symbol", + validations: [ + { + in: ["one", "of", "the", "above"], + }, + ], + }, + disabled: false, + omitted: false, + localized: false, + type: "Array", + }, + ], + description: "", + displayField: "", + name: "", + toPlainObject: () => ({} as ContentType), + }, + ] + + const locales: Locale[] = [ + { + name: "English (US)", + fallbackCode: null, + code: "en-US", + default: true, + sys: {} as Locale["sys"], + }, + { + name: "Brazilian Portuguese", + fallbackCode: "en-US", + code: "pt-BR", + default: false, + sys: {} as Locale["sys"], + }, + ] + + it("renders a given content type", async () => { + expect(await render(contentTypes, locales)).toMatchInlineSnapshot(` + "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + + import { Asset, Entry } from \\"contentful\\" + import { Document } from \\"@contentful/rich-text-types\\" + + export interface IMyContentTypeFields { + /** Array field */ + arrayField: (\\"one\\" | \\"of\\" | \\"the\\" | \\"above\\")[] + } + + export interface IMyContentType extends Entry { + sys: { + id: string + type: string + createdAt: string + updatedAt: string + locale: string + contentType: { + sys: { + id: \\"myContentType\\" + linkType: \\"ContentType\\" + type: \\"Link\\" + } + } + } + } + + export type CONTENT_TYPE = \\"myContentType\\" + + export type LOCALE_CODE = \\"en-US\\" | \\"pt-BR\\" + + export type CONTENTFUL_DEFAULT_LOCALE_CODE = \\"en-US\\" + " + `) + }) + + it("renders a given localized content type", async () => { const contentTypes: ContentType[] = [ { sys: { @@ -35,32 +122,15 @@ describe("render()", () => { }, ] - const locales: Locale[] = [ - { - name: "English (US)", - fallbackCode: null, - code: "en-US", - default: true, - sys: {} as Locale["sys"], - }, - { - name: "Brazilian Portuguese", - fallbackCode: "en-US", - code: "pt-BR", - default: false, - sys: {} as Locale["sys"], - }, - ] - - expect(await render(contentTypes, locales)).toMatchInlineSnapshot(` + expect(await render(contentTypes, locales, { localization: true })).toMatchInlineSnapshot(` "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. - import { Asset, Entry } from \\"contentful\\" + import { Entry } from \\"contentful\\" import { Document } from \\"@contentful/rich-text-types\\" export interface IMyContentTypeFields { /** Array field */ - arrayField: (\\"one\\" | \\"of\\" | \\"the\\" | \\"above\\")[] + arrayField: LocalizedField<(\\"one\\" | \\"of\\" | \\"the\\" | \\"above\\")[]> } export interface IMyContentType extends Entry { @@ -85,6 +155,30 @@ describe("render()", () => { export type LOCALE_CODE = \\"en-US\\" | \\"pt-BR\\" export type CONTENTFUL_DEFAULT_LOCALE_CODE = \\"en-US\\" + + export type LocalizedField = Partial> + + // We have to use our own localized version of Asset because of a bug in contentful https://github.com/contentful/contentful.js/issues/208 + export interface Asset { + sys: Sys + fields: { + title: LocalizedField + description: LocalizedField + file: LocalizedField<{ + url: string + details: { + size: number + image?: { + width: number + height: number + } + } + fileName: string + contentType: string + }> + } + toPlainObject(): object + } " `) }) diff --git a/test/renderers/typescript/renderInterfaceProperty.test.ts b/test/renderers/typescript/renderInterfaceProperty.test.ts index 09f6d3e0..b98b5d3c 100644 --- a/test/renderers/typescript/renderInterfaceProperty.test.ts +++ b/test/renderers/typescript/renderInterfaceProperty.test.ts @@ -2,22 +2,28 @@ import renderInterfaceProperty from "../../../src/renderers/typescript/renderInt describe("renderInterfaceProperty()", () => { it("works with unrequired properties", () => { - expect(renderInterfaceProperty("property", "type", false).trim()).toMatchInlineSnapshot( + expect(renderInterfaceProperty("property", "type", false, false).trim()).toMatchInlineSnapshot( `"property?: type | undefined;"`, ) }) it("works with required properties", () => { - expect(renderInterfaceProperty("property", "type", true).trim()).toMatchInlineSnapshot( + expect(renderInterfaceProperty("property", "type", true, false).trim()).toMatchInlineSnapshot( `"property: type;"`, ) }) it("adds descriptions", () => { - expect(renderInterfaceProperty("property", "type", false, "Description").trim()) + expect(renderInterfaceProperty("property", "type", false, false, "Description").trim()) .toMatchInlineSnapshot(` -"/** Description */ -property?: type | undefined;" -`) + "/** Description */ + property?: type | undefined;" + `) + }) + + it("supports localized fields", () => { + expect(renderInterfaceProperty("property", "type", false, true).trim()).toMatchInlineSnapshot( + `"property?: LocalizedField | undefined;"`, + ) }) })