Skip to content

Commit

Permalink
feat: add support for contentful.js v10 (#247)
Browse files Browse the repository at this point in the history

* chore: join imports from contentful
  • Loading branch information
veu authored Apr 25, 2023
1 parent 6b4b721 commit 7e360bf
Show file tree
Hide file tree
Showing 25 changed files with 1,169 additions and 19 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ OPTIONS
-h, --help show CLI help
-o, --out=out output directory
-p, --preserve preserve output folder
-X --v10 create contentful.js v10 types
-l, --localized add localized types
-d, --jsdoc add JSDoc comments
-g, --typeguard add type guards
Expand Down Expand Up @@ -278,10 +279,21 @@ import { CFDefinitionsBuilder, DefaultContentTypeRenderer } from 'cf-content-typ
const builder = new CFDefinitionsBuilder([new DefaultContentTypeRenderer()]);
```

## V10ContentTypeRenderer

A renderer to render type fields and entry definitions compatible with contentful.js v10.

```typescript
import { CFDefinitionsBuilder, V10ContentTypeRenderer } from 'cf-content-types-generator';

const builder = new CFDefinitionsBuilder([new V10ContentTypeRenderer()]);
```

## LocalizedContentTypeRenderer

Add additional types for localized fields. It adds utility types to transform fields into localized fields for given locales
More details on the utility types can be found here: [Issue 121](https://github.com/contentful-userland/cf-content-types-generator/issues/121)
Note that these types are not needed when using `V10ContentTypeRenderer` as the v10 entry type already supports localization.

#### Example Usage

Expand Down Expand Up @@ -381,7 +393,7 @@ Adds type guard functions for every content type
#### Example Usage

```typescript
import { CFDefinitionsBuilder, TypeGuardRenderer } from 'cf-content-types-generator';
import { CFDefinitionsBuilder, DefaultContentTypeRenderer, TypeGuardRenderer } from 'cf-content-types-generator';

const builder = new CFDefinitionsBuilder([
new DefaultContentTypeRenderer(),
Expand All @@ -406,6 +418,38 @@ export function isTypeAnimal(entry: WithContentTypeLink): entry is TypeAnimal {
}
```

## V10TypeGuardRenderer

Adds type guard functions for every content type which are compatible with contentful.js v10.

#### Example Usage

```typescript
import { CFDefinitionsBuilder, V10ContentTypeRenderer, V10TypeGuardRenderer } from 'cf-content-types-generator';

const builder = new CFDefinitionsBuilder([
new V10ContentTypeRenderer(),
new V10TypeGuardRenderer(),
]);
```

#### Example output

```typescript
import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from "contentful";

export interface TypeAnimalFields {
bread?: EntryFieldTypes.Symbol;
}

export type TypeAnimalSkeleton = EntrySkeletonType<TypeAnimalFields, "animal">;
export type TypeAnimal<Modifiers extends ChainModifiers, Locales extends LocaleCode> = Entry<TypeAnimalSkeleton, Modifiers, Locales>;

export function isTypeAnimal<Modifiers extends ChainModifiers, Locales extends LocaleCode>(entry: Entry<EntrySkeletonType, Modifiers, Locales>): entry is TypeAnimal<Modifiers, Locales> {
return entry.sys.contentType.sys.id === 'animal'
}
```

# 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`
Expand Down
15 changes: 13 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CFDefinitionsBuilder from './cf-definitions-builder';
import {
ContentTypeRenderer,
DefaultContentTypeRenderer,
V10ContentTypeRenderer,
JsDocRenderer,
LocalizedContentTypeRenderer,
TypeGuardRenderer,
} from './renderer';
import { V10TypeGuardRenderer } from './renderer/type/v10-type-guard-renderer';

// eslint-disable-next-line unicorn/prefer-module
const contentfulExport = require('contentful-export');
Expand All @@ -21,6 +23,7 @@ class ContentfulMdg extends Command {
help: flags.help({ char: 'h' }),
out: flags.string({ char: 'o', description: 'output directory' }),
preserve: flags.boolean({ char: 'p', description: 'preserve output folder' }),
v10: flags.boolean({ char: 'X', description: 'create contentful.js v10 types' }),
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' }),
Expand Down Expand Up @@ -65,8 +68,16 @@ class ContentfulMdg extends Command {
});
}

const renderers: ContentTypeRenderer[] = [new DefaultContentTypeRenderer()];
const renderers: ContentTypeRenderer[] = flags.v10
? [new V10ContentTypeRenderer()]
: [new DefaultContentTypeRenderer()];
if (flags.localized) {
if (flags.v10) {
this.error(
'"--localized" option is not needed, contentful.js v10 types have localization built in.',
);
}

renderers.push(new LocalizedContentTypeRenderer());
}

Expand All @@ -75,7 +86,7 @@ class ContentfulMdg extends Command {
}

if (flags.typeguard) {
renderers.push(new TypeGuardRenderer());
renderers.push(flags.v10 ? new V10TypeGuardRenderer() : new TypeGuardRenderer());
}

const builder = new CFDefinitionsBuilder(renderers);
Expand Down
4 changes: 4 additions & 0 deletions src/module-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export const moduleName = (name: string): string => {
return pipe([replaceDash, upperFirst, addPrefix, removeSpace])(name);
};

export const moduleSkeletonName = (name: string): string => {
return moduleName(name) + 'Skeleton';
};

export const moduleFieldsName = (name: string): string => {
return moduleName(name) + 'Fields';
};
11 changes: 6 additions & 5 deletions src/renderer/field/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from './render-types';
export { renderPropAny } from './render-prop-any';
export { renderPropArray } from './render-prop-array';
export { renderPropLink } from './render-prop-link';
export { renderPropResourceLink } from './render-prop-resource-link';
export { renderRichText } from './render-prop-richtext';
export { renderPropAny, renderPropAnyV10 } from './render-prop-any';
export { renderPropArray, renderPropArrayV10 } from './render-prop-array';
export { renderPropLink, renderPropLinkV10 } from './render-prop-link';
export { renderPropResourceLink, renderPropResourceLinkV10 } from './render-prop-resource-link';
export { renderRichText, renderRichTextV10 } from './render-prop-richtext';
export { defaultRenderers } from './default-renderers';
export { v10Renderers } from './v10-renderers';
30 changes: 29 additions & 1 deletion src/renderer/field/render-prop-any.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ContentTypeField } from 'contentful';
import { renderTypeLiteral, renderTypeUnion } from '../generic';
import { renderTypeGeneric, renderTypeLiteral, renderTypeUnion } from '../generic';
import { RenderContext } from '../type';

export const renderPropAny = (field: ContentTypeField, context: RenderContext): string => {
Expand All @@ -26,3 +26,31 @@ export const renderPropAny = (field: ContentTypeField, context: RenderContext):

return `EntryFields.${field.type}`;
};

export const renderPropAnyV10 = (field: ContentTypeField, context: RenderContext): string => {
context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['EntryFieldTypes'],
isTypeOnly: true,
});

if (field.validations?.length > 0) {
const includesValidation = field.validations.find((validation) => validation.in);
if (includesValidation && includesValidation.in) {
const mapper = (): ((value: string) => string) => {
if (field.type === 'Symbol' || field.type === 'Text' || field.type === 'RichText') {
return renderTypeLiteral;
}

return (value: string) => value.toString();
};

return renderTypeGeneric(
`EntryFieldTypes.${field.type}`,
renderTypeUnion(includesValidation.in.map((type) => mapper()(type))),
);
}
}

return `EntryFieldTypes.${field.type}`;
};
45 changes: 44 additions & 1 deletion src/renderer/field/render-prop-array.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ContentTypeField } from 'contentful';
import { inValidations } from '../../extract-validation';
import { renderTypeArray, renderTypeLiteral, renderTypeUnion } from '../generic';
import { renderTypeArray, renderTypeGeneric, renderTypeLiteral, renderTypeUnion } from '../generic';
import { RenderContext } from '../type';

export const renderPropArray = (field: ContentTypeField, context: RenderContext): string => {
Expand Down Expand Up @@ -40,3 +40,46 @@ export const renderPropArray = (field: ContentTypeField, context: RenderContext)

throw new Error('unhandled array type "' + field.items.type + '"');
};

export const renderPropArrayV10 = (field: ContentTypeField, context: RenderContext): string => {
if (!field.items) {
throw new Error(`missing items for ${field.id}`);
}

context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['EntryFieldTypes'],
isTypeOnly: true,
});

if (field.items.type === 'Link') {
return renderTypeGeneric(
'EntryFieldTypes.Array',
context.getFieldRenderer('Link')(field.items, context),
);
}

if (field.items.type === 'ResourceLink') {
return renderTypeGeneric(
'EntryFieldTypes.Array',
context.getFieldRenderer('ResourceLink')(field, context),
);
}

if (field.items.type === 'Symbol') {
const validation = inValidations(field.items);

if (validation?.length > 0) {
const validationsTypes = validation.map((val: string) => renderTypeLiteral(val));

return renderTypeGeneric(
'EntryFieldTypes.Array',
renderTypeGeneric('EntryFieldTypes.Symbol', renderTypeUnion(validationsTypes)),
);
}

return renderTypeGeneric('EntryFieldTypes.Array', 'EntryFieldTypes.Symbol');
}

throw new Error('unhandled array type "' + field.items.type + '"');
};
32 changes: 32 additions & 0 deletions src/renderer/field/render-prop-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,35 @@ export const renderPropLink = (
throw new Error(`Unknown linkType "${field.linkType}"`);
}
};

export const renderPropLinkV10 = (
field: Pick<ContentTypeField, 'validations' | 'linkType'>,
context: RenderContext,
): string => {
const linkContentType = (
field: Pick<ContentTypeField, 'validations'>,
context: RenderContext,
): string => {
const validations = linkContentTypeValidations(field);
return validations?.length > 0
? renderTypeUnion(validations.map((validation) => context.moduleSkeletonName(validation)))
: 'EntrySkeletonType';
};

context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['EntryFieldTypes'],
isTypeOnly: true,
});

switch (field.linkType) {
case 'Entry':
return renderTypeGeneric('EntryFieldTypes.EntryLink', linkContentType(field, context));

case 'Asset':
return 'EntryFieldTypes.AssetLink';

default:
throw new Error(`Unknown linkType "${field.linkType}"`);
}
};
19 changes: 19 additions & 0 deletions src/renderer/field/render-prop-resource-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,22 @@ export const renderPropResourceLink = (field: ContentTypeField, context: RenderC

return renderTypeGeneric('Entry', 'Record<string, any>');
};

export const renderPropResourceLinkV10 = (
field: ContentTypeField,
context: RenderContext,
): string => {
for (const resource of field.allowedResources!) {
if (resource.type !== 'Contentful:Entry') {
throw new Error(`Unknown type "${resource.type}"`);
}
}

context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['EntryFieldTypes', 'EntrySkeletonType'],
isTypeOnly: true,
});

return renderTypeGeneric('EntryFieldTypes.EntryResourceLink', 'EntrySkeletonType');
};
10 changes: 10 additions & 0 deletions src/renderer/field/render-prop-richtext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ export const renderRichText = (field: ContentTypeField, context: RenderContext):
});
return 'EntryFields.RichText';
};

export const renderRichTextV10 = (field: ContentTypeField, context: RenderContext): string => {
context.imports.add({
moduleSpecifier: 'contentful',
namedImports: ['EntryFieldTypes'],
isTypeOnly: true,
});

return 'EntryFieldTypes.RichText';
};
21 changes: 21 additions & 0 deletions src/renderer/field/v10-renderers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { renderPropAnyV10 } from './render-prop-any';
import { renderPropArrayV10 } from './render-prop-array';
import { renderPropLinkV10 } from './render-prop-link';
import { renderPropResourceLinkV10 } from './render-prop-resource-link';
import { renderRichTextV10 } from './render-prop-richtext';
import { FieldRenderers } from './render-types';

export const v10Renderers: FieldRenderers = {
RichText: renderRichTextV10,
Link: renderPropLinkV10,
ResourceLink: renderPropResourceLinkV10,
Array: renderPropArrayV10,
Text: renderPropAnyV10,
Symbol: renderPropAnyV10,
Object: renderPropAnyV10,
Date: renderPropAnyV10,
Number: renderPropAnyV10,
Integer: renderPropAnyV10,
Boolean: renderPropAnyV10,
Location: renderPropAnyV10,
};
4 changes: 2 additions & 2 deletions src/renderer/generic/render-type-generic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const renderTypeGeneric = (type: string, gen: string): string => {
return `${type}<${gen}>`;
export const renderTypeGeneric = (type: string, ...gen: string[]): string => {
return `${type}<${gen.join(', ')}>`;
};
4 changes: 3 additions & 1 deletion src/renderer/type/create-default-context.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { ContentTypeFieldType } from 'contentful';
import { ImportDeclarationStructure, OptionalKind } from 'ts-morph';
import { moduleFieldsName, moduleName } from '../../module-name';
import { moduleFieldsName, moduleName, moduleSkeletonName } from '../../module-name';
import { defaultRenderers, FieldRenderer } from '../field';

export type RenderContext = {
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) => FieldRenderer<FType>;
moduleName: (id: string) => string;
moduleFieldsName: (id: string) => string;
moduleSkeletonName: (id: string) => string;
imports: Set<OptionalKind<ImportDeclarationStructure>>;
};

export const createDefaultContext = (): RenderContext => {
return {
moduleName,
moduleFieldsName,
moduleSkeletonName,
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) =>
defaultRenderers[fieldType] as FieldRenderer<FType>,
imports: new Set(),
Expand Down
20 changes: 20 additions & 0 deletions src/renderer/type/create-v10-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ContentTypeFieldType } from 'contentful';
import { ImportDeclarationStructure, OptionalKind } from 'ts-morph';
import { FieldRenderer, v10Renderers } from '../field';
import { createDefaultContext } from './create-default-context';

export type RenderContext = {
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) => FieldRenderer<FType>;
moduleName: (id: string) => string;
moduleFieldsName: (id: string) => string;
moduleSkeletonName: (id: string) => string;
imports: Set<OptionalKind<ImportDeclarationStructure>>;
};

export const createV10Context = (): RenderContext => {
return {
...createDefaultContext(),
getFieldRenderer: <FType extends ContentTypeFieldType>(fieldType: FType) =>
v10Renderers[fieldType] as FieldRenderer<FType>,
};
};
Loading

0 comments on commit 7e360bf

Please sign in to comment.