Skip to content

API

Pinion has a small API with only a few tasks necessary to build powerful generators and CLI tools. This page has an overview of all available tasks, helpers and types.

Tasks

Tasks perform actions and possibly update the context with information that can be used in the next step. Most tasks take their arguments in two forms. Either as plain values like renderTemplate('This is the template string') or functions (or asynchronous functions) that get called with the current context and return the value, e.g.

ts
renderTemplate<Context>(
  (context) => `This is a dynamic template for ${context.name}`
)

prompt

prompt(options|context => options) takes a list of questions using inquirer.js and updates the context with the answers. A context callback can be used to only ask prompts conditionally (e.g. skipping when they have already been provided from the command line).

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

// The main types of your generator
export interface Context extends PinionContext {
  // Add the types from prompts and command line arguments here
  name: string
}

export const generate = (init: Context) =>
  Promise.resolve(init).then(
    prompt((context) => [
      {
        type: 'input',
        name: 'name',
        message: 'What is the name of your app?',
        // Only show prompt if there is no name
        when: !context.name
      }
    ])
  )

commander

commander(program|context => program) parses the generator command line arguments using a commander program and adds them to the context.

ts
import { PinionContext, commander, Command } from '../../src/index.js'

interface Context extends PinionContext {
  name: string
}

const program = new Command()
  .description('My awesome generator')
  .option('-n, --name <name>', 'Name of your project')

export const generate = (init: Context) =>
  Promise.resolve(init)
    .then(commander(program))
    .then((context) => {
      console.log(context.name)
    })

renderTemplate

renderTemplate(text|context => text, toFile, writeOptions) renders a string to a target file. writeOptions can be { force: true } to skip prompting if an an existing file should be overwritten.

To put together file names dynamically, the toFile helper can be used:

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

// The main types of your generator
export interface Context extends PinionContext {
  // Add the types from prompts and command line arguments here
  name: string
}

export const template = ({ name }: Context) =>
  `# ${name}

This is a readme generated by Pinion

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

export const generate = (init: Context) =>
  Promise.resolve(init)
    .then(
      prompt((context) => [
        {
          type: 'input',
          name: 'name',
          message: 'What is the name of your app?',
          // Only show prompt if there is no --name CLI argument
          when: !context.name
        }
      ])
    )
    .then(renderTemplate(template, toFile('readme.md')))

when

when(boolean|context => boolean, operation) evaluates a condition and runs the task if it returns true.

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

// The main types of your generator
export interface Context extends PinionContext {
  // Add the types from prompts and command line arguments here
  name: string
}

export const generate = (init: Context) =>
  Promise.resolve(init)
    .then(
      prompt((context) => [
        {
          type: 'input',
          name: 'name',
          message: 'What is the name of your app?',
          // Only show prompt if there is no name
          when: !context.name
        }
      ])
    )
    .then(
      when<Context>(
        ({ name }) => name === 'David',
        renderTemplate("I'm afraid I can't do that Dave", toFile('greeting.md'))
      )
    )

inject

inject(text|context => text, location, toFile) injects a template at a specific location into an existing file. The location functions can be used as follows:

  • before(text|context => text) inject at the line before text
  • after(text|context => text) inject at the line after text
  • prepend() inject at the beginning of the file
  • append() inject at the end of the file
ts
import {
  PinionContext,
  inject,
  before,
  prepend,
  toFile
} from '@featherscloud/pinion'

export const generate = (init: PinionContext) =>
  Promise.resolve(init)
    .then(
      inject(
        'Injected before copyright notice',
        before('Copyright (c)'),
        toFile('readme.md')
      )
    )
    .then(inject('Appended hello world', append(), toFile('readme.md')))

copyFiles

copyFiles(fromFile, toFile, options) recursively copies all files from a location to a destination. It will prompt to overwrite if a file already exists. options can be { force: true } to skip prompting if an an existing file should be overwritten.

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

export const generate = (init: PinionContext) =>
  Promise.resolve(init).then(
    copyFiles(fromFile(__dirname, 'static'), toFile('.'))
  )

writeJSON

writeJSON(data|context => data, toFile, writeOptions) write JSON data to a file. writeOptions can be { force: true } to skip prompting if an an existing file should be overwritten.

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

export const generate = (init: PinionContext) =>
  Promise.resolve(context).then(
    writeJSON({ description: 'Something' }, toFile('package.json'))
  )

mergeJSON

mergeJSON(data|context => data, toFile, writeOptions) merges new data into an existing file.

readJSON

readJSON(fromFile, converter, fallback?) loads a JSON file, parses it and extends the context with the data returned by converter.

ts
import { PackageJson } from 'type-fest'
import {
  PinionContext,
  writeJSON,
  readJSON,
  fromFile,
  toFile
} from '@featherscloud/pinion'

// The main types of your generator
export interface Context extends PinionContext {
  package: PackageJson
}

export const generate = (init: Context) =>
  Promise.resolve(init)
    // Load package.json, fall back to an empty object if it doesn't exist
    .then(readJSON(fromFile('package.json'), (package) => ({ package }), {}))
    // Merge existing package.json and write an updated description
    .then(
      writeJSON<Context>(
        ({ package }) => ({
          ...package,
          description: 'Overwritten description'
        }),
        toFile('package.json')
      )
    )

exec

exec(command|context => command, args|context => args) runs a command with command line arguments in the current working directory. It will error if the command returns an error exit code.

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

export const generate = (init: Context) =>
  generator(init).then(exec('npm', ['install', '@feathersjs/feathers']))

runGenerators

runGenerators((filePart|context => filePart)[]) will run all *.tpl.ts or *.tpl.js generators in the given path alphabetically in sequence, passing the current context.

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

export const generate = (init: Context) =>
  Promise.resolve(init).then(runGenerators(__dirname, 'templates'))

Helpers

file

file(...fileParts[]|context => fileParts[]) points to a filename, usually within the current working directory.

toFile

toFile(...fileParts[]|context => fileParts[]) works like file but will create the file and all folders that don't exist yet.

fromFile

fromFile(...fileParts[]|context => fileParts[]) works like file but makes sure that the file already exists.