Skip to content

Generators

A Pinion generator is any file that has a generate export which takes a context and returns a Promise with that context. In its most basic form a Pinion generator looks like this:

ts
import { PinionContext } from '@featherscloud/pinion'

// A Context interface. (This one is empty)
interface Context extends PinionContext {}

// The `generate` export
export const generate = (init: Context) => Promise.resolve(init)
sh
npx pinion generators/empty.tpl.ts

The steps in a generator will then be put together in a Promise chain (Promise.resolve(init).then(step1).then(step2)). Each step in the .then chain is called a Task.

Context

The key component of a Pinion generator is the context object that gets passed to each task. The Context interface defines the types of the variables used in generator tasks and templates.

Example

Let's look at the first quick start example and focus on the Context:

ts
import { PinionContext, renderTemplate, toFile } from '@featherscloud/pinion'

// A Context interface. (This one is empty)
interface Context extends PinionContext {}

// The file content as a template string
function readme() {
  return `# Hello world

This is a readme generated by Pinion

Copyright (c) ${new Date().getFullYear()}
`
}

// A `generate` export that wraps the context and renders the template
export function generate(init: Context) {
  return Promise.resolve(init).then(renderTemplate(readme, toFile('readme.md')))
}
sh
npx pinion generators/readme.tpl.ts

Even if your Context interface is empty, it still inherits the types from PinionContext. Let's see what's available in PinionContext.

PinionContext

Let's focus only on types. The Context should always extend PinionContext, as shown here:

ts
import { PinionContext } from '@featherscloud/pinion'

interface Context extends PinionContext {
  // ...
}

By extending PinionContext, your Context interface now has the following properties:

ts
export interface PinionContext {
  /**
   * The current working directory
   */
  cwd: string
  /**
   * The command line arguments
   */
  argv: string[]
  pinion: Configuration
}

Note

For more information on the pinion configuration see the Composability section.

Defining Context

You can add your own properties to Context. The process involves two steps:

  • Define the attribute in the Context interface.
  • Add the attribute to the context object somewhere in the generate function.

The first step is basically to define the type. The second one is to make the context match the type.

The next example is similar to the one at the top of the page but it moves the date string from the template into the context.

Let's review key lines to see how to further define Context:

  • Line 5: a number property named year is added to the Context interface.
  • Line 22: the object returned in the promise adds a year property to the context. This makes year available to subsequent functions and to the template. The renderTemplate utility then calls the template function with the updated context.
  • Line 9: the context is received by the template function.
  • Line 14: the year property is used in the template.
ts
import { PinionContext, toFile, renderTemplate } from '@featherscloud/pinion'

// A Context interface. (This one is empty)
interface Context extends PinionContext {
  year: number
}

// A function returning a template string
const readme = (context: Context) =>
`# Hello world

This is a readme generated by Pinion

Copyright (c) ${context.year}
`

// A `generate` export that wraps the context in a promise and renders it.
export const generate = (init: Context) => Promise.resolve(init)
  .then((context) => {
    return {
      ...context,
      year: new Date().getFullYear()
    }
  })
  .then(renderTemplate(readme, toFile('readme.md')))
sh
npx pinion generators/readme.tpl.ts

The above example is a bit contrived because we can use TypeScript functions directly in templates. It's not necessary to define variables outside of templates like it is with other generator tools.

The concept to be learned is that you can add properties to context in the promise and they will be available in the template. This would be more useful if we extracted the function into its own module and reused it in multiple generators.

What's really cool is that there's no special magic to learn. To make a reusable function, just export it from a .ts file. Then you can import it and use it like any other TypeScript function.

Tasks

A task is any step within the generate function. Pinion tasks rely on functional programming through Promise.then chains. You can follow each step in the generator and they are still bundled as a testable and embedable plain function.

ts
import { PinionContext, prompt } from '@featherscloud/pinion'

interface Context extends PinionContext {
  message: string
  name: string
  description: string
}

export function generate(init: Context) {
  return Promise.resolve(init)
    .then((context) => {
      // Returning a new object instead of modifying the existing object avoids
      // unintended side effects
      return {
        ...context,
        message: 'Updated context from task 1'
      }
    })
    .then(async (context) => {
      context.pinion.logger.log(
        `This is task 2 and context.message is: "${context.message}"`
      )

      return context
    })
    .then(
      prompt({
        name: {
          type: 'input',
          message: 'What is the name of your app?'
        },
        description: {
          type: 'input',
          message: 'Write a short description'
        }
      })
    )
    .then(async (context) => {
      context.pinion.logger.log(
        `Got name "${context.name}" and description "${context.description}"`
      )
      return context
    })
}
sh
npx pinion generators/tasks.tpl.ts

Note

A custom .then handler must always return the context. If you modify the context, return a new object as shown above.

Pinion has a few built in tasks to render templates, handle user input and. A list of all available tasks can be found in the API documentation. You can write your own functionality in a custom .then() task as shown above and write your own reusable tasks.

What's next?

In the next section we will talk about working with templates and files. The API has an overview of all built-in tasks.