Package Exports
- contentful-ts-generator
- contentful-ts-generator/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (contentful-ts-generator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
contentful-ts-generator
A CLI & webpack plugin for automatically generating Typescript code based on the content types in your Contentful space.
Installation:
npm install contentful-ts-generator
Usage:
CLI:
$ node_modules/.bin/contentful-ts-generator --help
Options:
--help Show help [boolean]
--version Show version number [boolean]
--file, -f The location on disk of the schema file.
--out, -o Where to place the generated code.
--download, -d Whether to download the schema file from the Contentful
space first [boolean]
--managementToken, -m The Contentful management token. Defaults to the env
var CONTENTFUL_MANAGEMENT_TOKEN
--space, -s The Contentful space ID. Defaults to the env var
CONTENTFUL_SPACE_ID
--environment, -e The Contentful environment. Defaults to the env var
CONTENTFUL_ENVIRONMENT or 'master'
It requires no parameters to function, provided you've set the appropriate environment
variables or have already downloaded a contentful-schema.json
file. By default,
in a Rails project it will look for db/contentful-schema.json
and generate
Typescript files in app/assets/javascripts/lib/contentful/generated
.
Webpack plugin
In your webpack.config.js:
const ContentfulTsGenerator = require('contentful-ts-generator')
module.exports = {
...
plugins: [
new ContentfulTsGenerator.ContentfulTsGeneratorPlugin({
/** (Optional) The location on disk of the schema file. */
schemaFile: 'db/contentful-schema.json',
/** (Optional) Where to place the generated code. */
outputDir: 'app/assets/javascripts/lib/contentful',
/**
* (Optional) Whether to download the schema file from the Contentful space first.
* This can take a long time - it's best to set this to "false" and commit your
* contentful-schema.json to the repository.
*/
downloadSchema: true,
/** (Optional) The Contentful space ID. Defaults to the env var CONTENTFUL_SPACE_ID */
space: '1xab...',
/** (Optional) The Contentful environment. Defaults to the env var CONTENTFUL_ENVIRONMENT or \'master\' */
environment: 'master',
/** (Optional) The Contentful management token. Defaults to the env var CONTENTFUL_MANAGEMENT_TOKEN */
managementToken: 'xxxx',
})
]
};
or in config/webpack/environment.js
for a
webpacker project
const { ContentfulTsGeneratorPlugin } = require('contentful-ts-generator')
environment.plugins.append('ContentfulTsGenerator', new ContentfulTsGeneratorPlugin({
// options
}))
Example:
import { ContentfulClientApi } from 'contentful'
import { Resolved } from './lib/contentful'
import {
IMenu
} from './lib/contentful/generated'
interface IProps {
menuId: string,
client: ContentfulClientApi
}
interface IState {
resolvedMenu: Resolved<IMenu>
}
export class MenuRenderer extends React.Component<IProps, IState> {
public componentDidMount() {
this.loadMenu()
}
public render() {
const { resolvedMenu } = this.state
if (!resolvedMenu) {
return <div className="waiting-indicator">Loading...</div>
}
return <div>
{resolvedMenu.fields.items.map(
// no need to cast here, the generated interface tells us it's an IMenuButton
(btn) => (
<a href={btn.fields.externalLink}>
<i className={btn.fields.ionIcon}>
{btn.fields.text}
</a>
)
)}
</div>
}
private async loadMenu() {
const { menuId, client } = this.props
// By default, client.getEntry resolves one level of links.
// This is represented with the `Resolved<IMenu>` type, which is what gets
// returned here.
const resolvedMenu = await client.getEntry<IMenu>(menuId)
this.setState({
resolvedMenu
})
}
}
What does 'generated/menu.ts'
look like?
Given a content type defined like this:
{
"sys": {
"id": "menu",
"type": "ContentType"
},
"displayField": "internalTitle",
"name": "Menu",
"description": "A Menu contains a number of Menu Buttons or other Menus, which will be rendered as drop-downs.",
"fields": [
{
"id": "internalTitle",
"name": "Internal Title (Contentful Only)",
"type": "Symbol",
"localized": false,
"required": true,
"validations": [],
"disabled": false,
"omitted": true
},
{
"id": "name",
"name": "Menu Name",
"type": "Symbol",
"localized": false,
"required": true,
"validations": [],
"disabled": false,
"omitted": false
},
{
"id": "items",
"name": "Items",
"type": "Array",
"localized": false,
"required": false,
"validations": [],
"disabled": false,
"omitted": false,
"items": {
"type": "Link",
"validations": [
{
"linkContentType": [
"cartButton",
"divider",
"dropdownMenu",
"loginButton",
"menuButton"
],
"message": "The items must be either buttons, drop-down menus, or dividers."
}
],
"linkType": "Entry"
}
}
]
}
The following types are generated:
import { wrap } from ".";
import { IEntry, ILink, isEntry, ISys } from "../base";
import { CartButton, ICartButton } from "./cart_button";
import { Divider, IDivider } from "./divider";
import { DropdownMenu, IDropdownMenu } from "./dropdown_menu";
import { ILoginButton, LoginButton } from "./login_button";
import { IMenuButton, MenuButton } from "./menu_button";
export interface IMenuFields {
internalTitle?: never;
name: string;
items?: Array<ILink<'Entry'> | MenuItem>;
}
export type MenuItem = ICartButton | IDivider | IDropdownMenu | ILoginButton | IMenuButton;
export type MenuItemClass = CartButton | Divider | DropdownMenu | LoginButton | MenuButton;
/**
* Menu
* A Menu contains a number of Menu Buttons or other Menus, which will be rendered as drop-downs.
*/
export interface IMenu extends IEntry<IMenuFields> {
}
export function isMenu(entry: IEntry<any>): entry is IMenu {
return entry &&
entry.sys &&
entry.sys.contentType &&
entry.sys.contentType.sys &&
entry.sys.contentType.sys.id == 'menu'
}
export class Menu implements IMenu {
public readonly sys!: ISys<'Entry'>;
public readonly fields!: IMenuFields;
get name(): string {
return this.fields.name
}
get items(): Array<MenuItemClass | null> | undefined {
return !this.fields.items ? undefined :
this.fields.items.map((item) =>
isEntry(item) ? wrap<'cartButton' | 'divider' | 'dropdownMenu' | 'loginButton' | 'menuButton'>(item) : null
)
}
constructor(entry: IMenu);
constructor(id: string, fields: IMenuFields);
constructor(entryOrId: IMenu | string, fields?: IMenuFields) {
if (typeof entryOrId == 'string') {
if (!fields) {
throw new Error('No fields provided')
}
this.sys = {
id: entryOrId,
type: 'Entry',
space: undefined,
contentType: {
sys: {
type: 'Link',
linkType: 'ContentType',
id: 'menu'
}
}
}
this.fields = fields
} else {
if (typeof entryOrId.sys == 'undefined') {
throw new Error('Entry did not have a `sys`!')
}
if (typeof entryOrId.fields == 'undefined') {
throw new Error('Entry did not have a `fields`!')
}
Object.assign(this, entryOrId)
}
}
}
The interface represents data coming back from Contentful's getEntry
SDK function.
The generated class can be used as a convenient wrapper. For example:
const menu = new Menu(await client.getEntry('my-menu-id'))
const button0 = menu.items[0]
expect(button0.text).to.equal('About Us')
You can also extend the generated classes with your own functions and properties.
As an example, suppose you wanted to use some client-side logic to determine
whether a certain menu button should be hidden from users. You could define
an accessLevel
property on menu button:
// in lib/contentful/ext/menu_button.ts
import { MenuButton } from '../generated/menu_button'
// reopen the MenuButton module to add properties and functions to
// the Typescript definition
declare module '../generated/menu_button' {
export interface MenuButton {
accessLevel: number
}
}
const restrictedPages: Array<[RegExp, number]> = [
[/^admin/, 9],
]
// Define a javascript property which becomes the actual
// property implementation
Object.defineProperty(MenuButton.prototype, 'accessLevel', {
get() {
const slug: string = this.link && isEntry(this.link) ?
this.link.slug :
this.externalLink
if (!slug) {
return 0
}
for (const restriction of restrictedPages) {
const test = restriction[0]
const accessLevel = restriction[1]
if (test.test(slug)) {
return restriction[1]
}
return 0
}
},
enumerable: true,
})
And using it in your react component:
import { Menu } from './lib/contentful/generated'
interface IProps {
resolvedMenu: Menu,
currentUser: {
accessLevel: number
}
}
export class MenuRenderer extends React.Component<IProps> {
public render() {
const { resolvedMenu, currentUser } = this.props
return <div>
{
resolvedMenu.items
// Here we only show the buttons that the current user has access to see.
// Since `resolvedMenu` is an instance of Menu, its `items` field contains
// only MenuButton instances, which have our property defined on them.
.filter((btn) => currentUser.accessLevel >= btn.accessLevel)
.map((btn) => (
<a href={btn.externalLink}>
<i className={btn.ionIcon}>
{btn.text}
</a>
))
}
</div>
}
}
This is a cleaner implementation than putting the access level logic in the view.