diff --git a/README.md b/README.md index 54e02b12..d2fe9367 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,32 @@ module.exports = function() { } ``` +Optionally, you can add a file called `getContentfulFieldOverrides.js` in the root of your project, which should export two functions: +1. `getImports` which should return an array of import declarations (as strings) required for your overriden types. +2. `getOverridenContentTypes` that returns an object of `OverridenContentTypes` type (see example), which will indicate which fields for which content types should be overriden with the type name you provided. + +Example: +```js +function getImports() { + return [ + "import { SomeLibraryType } from '@some-library';" + ] +} + +function getOverridenContentTypes() { + return { + 'myContentTypeId': { + 'overridenField': 'SomeLibraryType' + } + } +} + +module.exports = { + getImports, + getOverridenContentTypes, +} +``` + ### Command line options ``` diff --git a/src/contentful-typescript-codegen.ts b/src/contentful-typescript-codegen.ts index f97e1ede..6a7c2f17 100644 --- a/src/contentful-typescript-codegen.ts +++ b/src/contentful-typescript-codegen.ts @@ -2,6 +2,7 @@ import render from "./renderers/render" import renderFieldsOnly from "./renderers/renderFieldsOnly" import path from "path" import { outputFileSync } from "fs-extra" +import { OverridenContentTypes } from "./lib/fieldOverrides" const meow = require("meow") @@ -67,13 +68,38 @@ async function runCodegen(outputFile: string) { const locales = await environment.getLocales() const outputPath = path.resolve(process.cwd(), outputFile) + let fieldOverrides: OverridenContentTypes | undefined + let extraImports: string[] | undefined + try { + const getContentfulFieldOverridesPath = path.resolve( + process.cwd(), + "./getContentfulFieldOverrides.js", + ) + const { getImports, getOverridenContentTypes } = require(getContentfulFieldOverridesPath) + extraImports = getImports() as string[] + fieldOverrides = getOverridenContentTypes() as OverridenContentTypes + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + console.warn("`getContentfulFieldOverrides` file not found, skipping...") + fieldOverrides = undefined + extraImports = undefined + } else { + throw error + } + } + let output if (cli.flags.fieldsOnly) { - output = await renderFieldsOnly(contentTypes.items, { namespace: cli.flags.namespace }) + output = await renderFieldsOnly(contentTypes.items, { + namespace: cli.flags.namespace, + fieldOverrides, + }) } else { output = await render(contentTypes.items, locales.items, { localization: cli.flags.localization, namespace: cli.flags.namespace, + fieldOverrides, + extraImports, }) } diff --git a/src/lib/fieldOverrides.ts b/src/lib/fieldOverrides.ts new file mode 100644 index 00000000..2637941b --- /dev/null +++ b/src/lib/fieldOverrides.ts @@ -0,0 +1,20 @@ +import { ContentType, Field } from "contentful" + +export type OverridenContentTypes = Record + +export type OverridenFields = Record + +export function getOverridenFields( + contentType: ContentType, + fieldOverrides?: OverridenContentTypes, +) { + return fieldOverrides ? fieldOverrides[contentType.sys.id] : undefined +} + +export function getOverridenFieldType(field: Field, overridenFields?: OverridenFields) { + if (!overridenFields) { + return undefined + } + + return overridenFields[field.id] +} diff --git a/src/renderers/contentful-fields-only/renderContentType.ts b/src/renderers/contentful-fields-only/renderContentType.ts index 2394bd04..42328714 100644 --- a/src/renderers/contentful-fields-only/renderContentType.ts +++ b/src/renderers/contentful-fields-only/renderContentType.ts @@ -1,5 +1,11 @@ import { ContentType, Field, FieldType } from "contentful" +import { + getOverridenFields, + getOverridenFieldType, + OverridenContentTypes, + OverridenFields, +} from "../../lib/fieldOverrides" import renderInterface from "../typescript/renderInterface" import renderField from "../contentful/renderField" import renderContentTypeId from "../contentful/renderContentTypeId" @@ -14,9 +20,13 @@ import renderNumber from "../contentful/fields/renderNumber" import renderObject from "../contentful/fields/renderObject" import renderSymbol from "../contentful/fields/renderSymbol" -export default function renderContentType(contentType: ContentType): string { +export default function renderContentType( + contentType: ContentType, + fieldOverrides?: OverridenContentTypes, +): string { const name = renderContentTypeId(contentType.sys.id) - const fields = renderContentTypeFields(contentType.fields) + const overridenFields = getOverridenFields(contentType, fieldOverrides) + const fields = renderContentTypeFields(contentType.fields, overridenFields) return renderInterface({ name, @@ -27,10 +37,12 @@ export default function renderContentType(contentType: ContentType): string { }) } -function renderContentTypeFields(fields: Field[]): string { +function renderContentTypeFields(fields: Field[], overridenFields?: OverridenFields): string { return fields .filter(field => !field.omitted) .map(field => { + const overridenType = getOverridenFieldType(field, overridenFields) + const functionMap: Record string> = { Array: renderArray, Boolean: renderBoolean, @@ -45,7 +57,7 @@ function renderContentTypeFields(fields: Field[]): string { Text: renderSymbol, } - return renderField(field, functionMap[field.type](field)) + return renderField(field, overridenType || functionMap[field.type](field)) }) .join("\n\n") } diff --git a/src/renderers/contentful/renderContentType.ts b/src/renderers/contentful/renderContentType.ts index 456cf8eb..caeb7693 100644 --- a/src/renderers/contentful/renderContentType.ts +++ b/src/renderers/contentful/renderContentType.ts @@ -1,5 +1,11 @@ import { ContentType, Field, FieldType, Sys } from "contentful" +import { + getOverridenFields, + getOverridenFieldType, + OverridenContentTypes, + OverridenFields, +} from "../../lib/fieldOverrides" import renderInterface from "../typescript/renderInterface" import renderField from "./renderField" import renderContentTypeId from "./renderContentTypeId" @@ -13,9 +19,14 @@ import renderObject from "./fields/renderObject" import renderRichText from "./fields/renderRichText" import renderSymbol from "./fields/renderSymbol" -export default function renderContentType(contentType: ContentType, localization: boolean): string { +export default function renderContentType( + contentType: ContentType, + localization: boolean, + fieldOverrides?: OverridenContentTypes, +): string { const name = renderContentTypeId(contentType.sys.id) - const fields = renderContentTypeFields(contentType.fields, localization) + const overridenFields = getOverridenFields(contentType, fieldOverrides) + const fields = renderContentTypeFields(contentType.fields, localization, overridenFields) const sys = renderSys(contentType.sys) return ` @@ -34,10 +45,16 @@ function descriptionComment(description: string | undefined) { return "" } -function renderContentTypeFields(fields: Field[], localization: boolean): string { +function renderContentTypeFields( + fields: Field[], + localization: boolean, + overridenFields?: OverridenFields, +): string { return fields .filter(field => !field.omitted) .map(field => { + const overridenType = getOverridenFieldType(field, overridenFields) + const functionMap: Record string> = { Array: renderArray, Boolean: renderBoolean, @@ -52,7 +69,7 @@ function renderContentTypeFields(fields: Field[], localization: boolean): string Text: renderSymbol, } - return renderField(field, functionMap[field.type](field), localization) + return renderField(field, overridenType || functionMap[field.type](field), localization) }) .join("\n\n") } diff --git a/src/renderers/render.ts b/src/renderers/render.ts index 6b056eef..f2076082 100644 --- a/src/renderers/render.ts +++ b/src/renderers/render.ts @@ -2,29 +2,32 @@ import { ContentType, Locale } from "contentful" import { format, resolveConfig } from "prettier" -import renderContentfulImports from "./contentful/renderContentfulImports" +import renderImports from "./renderImports" import renderContentType from "./contentful/renderContentType" import renderUnion from "./typescript/renderUnion" import renderAllLocales from "./contentful/renderAllLocales" import renderDefaultLocale from "./contentful/renderDefaultLocale" import renderNamespace from "./contentful/renderNamespace" import renderLocalizedTypes from "./contentful/renderLocalizedTypes" +import { OverridenContentTypes } from "../lib/fieldOverrides" interface Options { localization?: boolean namespace?: string + fieldOverrides?: OverridenContentTypes + extraImports?: string[] } export default async function render( contentTypes: ContentType[], locales: Locale[], - { namespace, localization = false }: Options = {}, + { namespace, localization = false, fieldOverrides, extraImports }: 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 typingsSource = [ - renderAllContentTypes(sortedContentTypes, localization), + renderAllContentTypes(sortedContentTypes, localization, fieldOverrides), renderAllContentTypeIds(sortedContentTypes), renderAllLocales(sortedLocales), renderDefaultLocale(sortedLocales), @@ -32,7 +35,7 @@ export default async function render( ].join("\n\n") const source = [ - renderContentfulImports(localization), + renderImports(localization, extraImports), renderNamespace(typingsSource, namespace), ].join("\n\n") @@ -40,10 +43,19 @@ export default async function render( return format(source, { ...prettierConfig, parser: "typescript" }) } -function renderAllContentTypes(contentTypes: ContentType[], localization: boolean): string { - return contentTypes.map(contentType => renderContentType(contentType, localization)).join("\n\n") +function renderAllContentTypes( + contentTypes: ContentType[], + localization: boolean, + fieldOverrides?: OverridenContentTypes, +): string { + return contentTypes + .map(contentType => renderContentType(contentType, localization, fieldOverrides)) + .join("\n\n") } function renderAllContentTypeIds(contentTypes: ContentType[]): string { - return renderUnion("CONTENT_TYPE", contentTypes.map(contentType => `'${contentType.sys.id}'`)) + return renderUnion( + "CONTENT_TYPE", + contentTypes.map(contentType => `'${contentType.sys.id}'`), + ) } diff --git a/src/renderers/renderFieldsOnly.ts b/src/renderers/renderFieldsOnly.ts index 6cae24d3..e3e68a64 100644 --- a/src/renderers/renderFieldsOnly.ts +++ b/src/renderers/renderFieldsOnly.ts @@ -2,20 +2,22 @@ import { ContentType } from "contentful" import { format, resolveConfig } from "prettier" +import { OverridenContentTypes } from "../lib/fieldOverrides" import renderContentType from "./contentful-fields-only/renderContentType" import renderNamespace from "./contentful/renderNamespace" interface Options { namespace?: string + fieldOverrides?: OverridenContentTypes } export default async function renderFieldsOnly( contentTypes: ContentType[], - { namespace }: Options = {}, + { namespace, fieldOverrides }: Options = {}, ) { const sortedContentTypes = contentTypes.sort((a, b) => a.sys.id.localeCompare(b.sys.id)) - const typingsSource = renderAllContentTypes(sortedContentTypes) + const typingsSource = renderAllContentTypes(sortedContentTypes, fieldOverrides) const source = [renderNamespace(typingsSource, namespace)].join("\n\n") const prettierConfig = await resolveConfig(process.cwd()) @@ -23,6 +25,11 @@ export default async function renderFieldsOnly( return format(source, { ...prettierConfig, parser: "typescript" }) } -function renderAllContentTypes(contentTypes: ContentType[]): string { - return contentTypes.map(contentType => renderContentType(contentType)).join("\n\n") +function renderAllContentTypes( + contentTypes: ContentType[], + fieldOverrides?: OverridenContentTypes, +): string { + return contentTypes + .map(contentType => renderContentType(contentType, fieldOverrides)) + .join("\n\n") } diff --git a/src/renderers/renderImports.ts b/src/renderers/renderImports.ts new file mode 100644 index 00000000..b0237d7b --- /dev/null +++ b/src/renderers/renderImports.ts @@ -0,0 +1,18 @@ +import renderContentfulImports from "./contentful/renderContentfulImports" + +function renderExtraImports(extraImports: string[]) { + return extraImports.join("\n") +} + +export default function renderImports( + localization: boolean = false, + extraImports?: string[], +): string { + const contentfulImports = renderContentfulImports(localization) + + if (!extraImports) { + return contentfulImports + } + + return [contentfulImports, renderExtraImports(extraImports)].join("\n\n") +} diff --git a/test/renderers/contentful-fields-only/renderContentType.test.ts b/test/renderers/contentful-fields-only/renderContentType.test.ts index fb73fba1..215937d0 100644 --- a/test/renderers/contentful-fields-only/renderContentType.test.ts +++ b/test/renderers/contentful-fields-only/renderContentType.test.ts @@ -1,8 +1,15 @@ import renderContentType from "../../../src/renderers/contentful-fields-only/renderContentType" import { ContentType, Sys } from "contentful" import format from "../../support/format" +import { OverridenContentTypes } from "../../../src/lib/fieldOverrides" describe("renderContentType()", () => { + const fieldOverrides: OverridenContentTypes = { + myContentType: { + symbolField: "MyCustomType", + }, + } + const contentType: ContentType = { sys: { id: "myContentType", @@ -57,4 +64,19 @@ describe("renderContentType()", () => { }" `) }) + + it("works with miscellaneous field types", () => { + expect(format(renderContentType(contentType, fieldOverrides))).toMatchInlineSnapshot(` + "export interface IMyContentType { + fields: { + /** Symbol Field™ */ + symbolField?: MyCustomType | undefined, + + /** Array field */ + arrayField: (\\"one\\" | \\"of\\" | \\"the\\" | \\"above\\")[] + }; + [otherKeys: string]: any; + }" + `) + }) }) diff --git a/test/renderers/contentful/renderContentType.test.ts b/test/renderers/contentful/renderContentType.test.ts index af9df218..c08fff22 100644 --- a/test/renderers/contentful/renderContentType.test.ts +++ b/test/renderers/contentful/renderContentType.test.ts @@ -1,8 +1,15 @@ import renderContentType from "../../../src/renderers/contentful/renderContentType" import { ContentType, Sys } from "contentful" import format from "../../support/format" +import { OverridenContentTypes } from "../../../src/lib/fieldOverrides" describe("renderContentType()", () => { + const fieldOverrides: OverridenContentTypes = { + myContentType: { + symbolField: "MyCustomType", + }, + } + const contentType: ContentType = { sys: { id: "myContentType", @@ -136,4 +143,33 @@ describe("renderContentType()", () => { }" `) }) + + it("works with field overrides", () => { + expect(format(renderContentType(contentType, false, fieldOverrides))).toMatchInlineSnapshot(` + "export interface IMyContentTypeFields { + /** Symbol Field™ */ + symbolField?: MyCustomType | undefined; + + /** 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\\" + } + } + }; + }" + `) + }) }) diff --git a/test/renderers/contentful/renderContentfulImports.test.ts b/test/renderers/contentful/renderContentfulImports.test.ts deleted file mode 100644 index 5c8f3c87..00000000 --- a/test/renderers/contentful/renderContentfulImports.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import renderContentfulImports from "../../../src/renderers/contentful/renderContentfulImports" -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. - - 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/contentful/renderImports.test.ts b/test/renderers/contentful/renderImports.test.ts new file mode 100644 index 00000000..0906f907 --- /dev/null +++ b/test/renderers/contentful/renderImports.test.ts @@ -0,0 +1,36 @@ +import renderImports from "../../../src/renderers/renderImports" +import format from "../../support/format" + +const TEST_IMPORTS = ["import Foo from './bar';", "import { Bar } from '@baz';"] + +describe("renderContentfulImports()", () => { + it("renders the top of the codegen file", () => { + expect(format(renderImports())).toMatchInlineSnapshot(` + "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + + import { Asset, Entry } from \\"contentful\\"; + import { Document } from \\"@contentful/rich-text-types\\";" + `) + }) + + it("renders the localized top of the codegen file", () => { + expect(format(renderImports(true))).toMatchInlineSnapshot(` + "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + + import { Entry } from \\"contentful\\"; + import { Document } from \\"@contentful/rich-text-types\\";" + `) + }) + + it("renders the top of the codegen file with extra imports", () => { + expect(format(renderImports(false, TEST_IMPORTS))).toMatchInlineSnapshot(` + "// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY IT. + + import { Asset, Entry } from \\"contentful\\"; + import { Document } from \\"@contentful/rich-text-types\\"; + + import Foo from \\"./bar\\"; + import { Bar } from \\"@baz\\";" + `) + }) +})