JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 365
  • Score
    100M100P100Q93639F
  • License MIT

A CLI & webpack plugin for automatically generating Typescript code based on the content types in your Contentful space.

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

npm version Build Status Coverage Status

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.