Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ jobs:

- run:
name: Run tests
command: yarn test --ci

command: yarn test:ci

release:
<<: *defaults
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"start": "rollup -c rollup.config.ts -w",
"test": "jest --coverage",
"test:ci": "jest --coverage --max-workers=2 --ci",
"test:prod": "npm run lint && npm run test -- --no-cache",
"test:watch": "jest --coverage --watch",
"semantic-release": "semantic-release"
Expand Down
29 changes: 22 additions & 7 deletions src/contentful-typescript-codegen.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import render from "./renderers/render"
import renderFieldsOnly from "./renderers/renderFieldsOnly"
import path from "path"
import { outputFileSync } from "fs-extra"

const meow = require("meow")

const cli = meow(
`
Usage
$ contentful-typescript-codegen --output <file> <options>
Usage
$ contentful-typescript-codegen --output <file> <options>

Options
--output, -o Where to write to
Options
--output, -o Where to write to
--poll, -p Continuously refresh types
--interval N, -i The interval in seconds at which to poll (defaults to 15)
--fields-only Output a tree that _only_ ensures fields are valid
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.

Examples
$ contentful-typescript-codegen -o src/@types/generated/contentful.d.ts
Examples
$ contentful-typescript-codegen -o src/@types/generated/contentful.d.ts
`,
{
flags: {
Expand All @@ -24,6 +29,10 @@ const cli = meow(
alias: "o",
required: true,
},
fieldsOnly: {
type: "boolean",
required: false,
},
poll: {
type: "boolean",
alias: "p",
Expand All @@ -44,9 +53,15 @@ async function runCodegen(outputFile: string) {
const environment = await getEnvironment()
const contentTypes = await environment.getContentTypes({ limit: 1000 })
const locales = await environment.getLocales()
const output = await render(contentTypes.items, locales.items)
const outputPath = path.resolve(process.cwd(), outputFile)

let output
if (cli.flags.fieldsOnly) {
output = await renderFieldsOnly(contentTypes.items)
} else {
output = await render(contentTypes.items, locales.items)
}

outputFileSync(outputPath, output)
}

Expand Down
28 changes: 28 additions & 0 deletions src/renderers/contentful-fields-only/fields/renderArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Field } from "contentful"
import renderSymbol from "../../contentful/fields/renderSymbol"
import renderLink from "../../contentful-fields-only/fields/renderLink"
import renderArrayOf from "../../typescript/renderArrayOf"

export default function renderArray(field: Field): string {
if (!field.items) {
throw new Error(`Cannot render non-array field ${field.id} as an array`)
}

const fieldWithValidations: Field = {
...field,
linkType: field.items.linkType,
validations: field.items.validations || [],
}

switch (field.items.type) {
case "Symbol": {
return renderArrayOf(renderSymbol(fieldWithValidations))
}

case "Link": {
return renderArrayOf(renderLink(fieldWithValidations))
}
}

return renderArrayOf("unknown")
}
21 changes: 21 additions & 0 deletions src/renderers/contentful-fields-only/fields/renderLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Field } from "contentful"
import renderContentTypeId from "../../contentful/renderContentTypeId"
import { renderUnionValues } from "../../typescript/renderUnion"

export default function renderLink(field: Field): string {
if (field.linkType === "Asset") {
return "any"
}

if (field.linkType === "Entry") {
const contentTypeValidation = field.validations.find(validation => !!validation.linkContentType)

if (contentTypeValidation) {
return renderUnionValues(contentTypeValidation.linkContentType!.map(renderContentTypeId))
} else {
return "unknown"
}
}

return "unknown"
}
5 changes: 5 additions & 0 deletions src/renderers/contentful-fields-only/fields/renderRichText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Field } from "contentful"

export default function renderRichText(field: Field): string {
return "any"
}
51 changes: 51 additions & 0 deletions src/renderers/contentful-fields-only/renderContentType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ContentType, Field, FieldType } from "contentful"

import renderInterface from "../typescript/renderInterface"
import renderField from "../contentful/renderField"
import renderContentTypeId from "../contentful/renderContentTypeId"

import renderArray from "../contentful-fields-only/fields/renderArray"
import renderLink from "../contentful-fields-only/fields/renderLink"
import renderRichText from "../contentful-fields-only/fields/renderRichText"

import renderBoolean from "../contentful/fields/renderBoolean"
import renderLocation from "../contentful/fields/renderLocation"
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 {
const name = renderContentTypeId(contentType.sys.id)
const fields = renderContentTypeFields(contentType.fields)

return renderInterface({
name,
fields: `
fields: { ${fields} };
[otherKeys: string]: any;
`,
})
}

function renderContentTypeFields(fields: Field[]): string {
return fields
.filter(field => !field.omitted)
.map<string>(field => {
const functionMap: Record<FieldType, (field: Field) => string> = {
Array: renderArray,
Boolean: renderBoolean,
Date: renderSymbol,
Integer: renderNumber,
Link: renderLink,
Location: renderLocation,
Number: renderNumber,
Object: renderObject,
RichText: renderRichText,
Symbol: renderSymbol,
Text: renderSymbol,
}

return renderField(field, functionMap[field.type](field))
})
.join("\n\n")
}
26 changes: 19 additions & 7 deletions src/renderers/contentful/renderContentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,25 @@ import renderObject from "./fields/renderObject"
import renderRichText from "./fields/renderRichText"
import renderSymbol from "./fields/renderSymbol"

export default function renderContentType(contentType: ContentType) {
return renderInterface(
renderContentTypeId(contentType.sys.id),
renderContentTypeFields(contentType.fields),
contentType.description,
renderSys(contentType.sys),
)
export default function renderContentType(contentType: ContentType): string {
const name = renderContentTypeId(contentType.sys.id)
const fields = renderContentTypeFields(contentType.fields)
const sys = renderSys(contentType.sys)

return `
${renderInterface({ name: `${name}Fields`, fields })}

${descriptionComment(contentType.description)}
${renderInterface({ name, extension: `Entry<${name}Fields>`, fields: sys })}
`
}

function descriptionComment(description: string | undefined) {
if (description) {
return `/** ${description} */`
}

return ""
}

function renderContentTypeFields(fields: Field[]): string {
Expand Down
17 changes: 17 additions & 0 deletions src/renderers/renderFieldsOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ContentType } from "contentful"

import { format } from "prettier"

import renderContentType from "./contentful-fields-only/renderContentType"

export default async function renderFieldsOnly(contentTypes: ContentType[]) {
const sortedContentTypes = contentTypes.sort((a, b) => a.sys.id.localeCompare(b.sys.id))

const source = renderAllContentTypes(sortedContentTypes)

return format(source, { parser: "typescript" })
}

function renderAllContentTypes(contentTypes: ContentType[]): string {
return contentTypes.map(contentType => renderContentType(contentType)).join("\n\n")
}
35 changes: 14 additions & 21 deletions src/renderers/typescript/renderInterface.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
export default function renderInterface(
name: string,
fields: string,
description?: string,
contents?: string,
): string {
export default function renderInterface({
name,
extension,
fields,
description,
}: {
name: string
extension?: string
fields: string
description?: string
}) {
return `
export interface ${name}Fields {
${description ? `/** ${description} */` : ""}
export interface ${name} ${extension ? `extends ${extension}` : ""} {
${fields}
};

${descriptionComment(description)}
export interface ${name} extends Entry<${name}Fields> {
${contents || ""}
};
}
`
}

function descriptionComment(description: string | undefined) {
if (description) {
return `/** ${description} */`
}

return ""
}
69 changes: 69 additions & 0 deletions test/renderers/contentful-fields-only/fields/renderArray.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import renderArray from "../../../../src/renderers/contentful-fields-only/fields/renderArray"
import { Field } from "contentful"

describe("renderArray()", () => {
it("renders an array of symbols", () => {
const arrayOfSymbols: Field = {
type: "Array",
id: "fieldId",
name: "Field Name",
validations: [],
omitted: false,
required: true,
disabled: false,
linkType: undefined,
localized: false,
items: {
type: "Symbol",
validations: [],
},
}

expect(renderArray(arrayOfSymbols)).toMatchInlineSnapshot(`"(string)[]"`)
})

it("renders an array of symbols with validations", () => {
const arrayOfValidatedSymbols: Field = {
type: "Array",
id: "fieldId",
name: "Field Name",
validations: [],
omitted: false,
required: true,
disabled: false,
linkType: undefined,
localized: false,
items: {
type: "Symbol",
validations: [{ in: ["one", "of", "these"] }],
},
}

expect(renderArray(arrayOfValidatedSymbols)).toMatchInlineSnapshot(
`"('one' | 'of' | 'these')[]"`,
)
})

it("renders an array of links of a particular type", () => {
const arrayOfValidatedSymbols: Field = {
type: "Array",
id: "fieldId",
name: "Field Name",
validations: [],
omitted: false,
required: true,
disabled: false,
linkType: undefined,
localized: false,
items: {
type: "Link",
linkType: "Entry",
validations: [{ linkContentType: ["contentType1", "contentType2"] }],
},
}

expect(renderArray(arrayOfValidatedSymbols)).toMatchInlineSnapshot(
`"(IContentType1 | IContentType2)[]"`,
)
})
})
Loading