Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for contentful.js v10 #247

Merged
merged 5 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
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