Skip to content

Templates and Files

Pinion is unique. Templates are written in plain TypeScript, so they're super easy to use! This page covers how to create templates, how to write them to files and how to inject templates into existing files.

Files

There are three main helpers for working with file pointers:

  • file - A generic pointer to a file like file('readme.md'), file('docs', 'readme.md') or file(context => [context.docsPath, 'readme.md'])
  • fromFile - Like file but makes sure that the file already exists
  • toFile - Like file but will create the file including missing directories if it does not exist

The following example asks for the path the readme.md file should be generated in and then puts the path together dynamically based on the context:

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

// Setup the Context to receive user input
interface Context extends PinionContext {
  name: string
  description: string
  docsPath: string
}

// The template uses Context variables.
function readme({ name, description }: Context) {
  return `# ${name}

> ${description}

This is a readme generated by Pinion

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

export function generate(init: Context) {
  return Promise.resolve(init)
    .then(
      prompt({
        name: {
          type: 'input',
          message: 'What is the name of your app?'
        },
        description: {
          type: 'input',
          message: 'Write a short description'
        },
        docsPath: {
          type: 'input',
          default: 'docs',
          message: 'Where should the documentation live?'
        }
      })
    )
    // Render the template
    .then(
      renderTemplate(
        readme,
        toFile(context => [context.docsPath, 'readme.md'])
      )
    )
}
sh
npx pinion generators/readme.tpl.ts

The built-in file related tasks are:

Templating

While you could use them in your own tasks, Pinion tries to avoid difficult-to-debug and untyped templating languages like EJS or Mustache. Instead, a standard Pinion template is simply a TypeScript template string. This means that templates are automatically typed and compiled with the rest of the generator. In our experience this avoids many runtime errors while also being nicer to develop. You get full auto-completion in any TypeScript code editor.

It also allows composing templates by splitting them up into separate functions, and you have the entire JavaScript ecosystem (like Lodash) available as helpers:

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

interface Context extends PinionContext {
  name: string
}

export function copyrightTemplate() {
  return `Copyright (c) ${new Date().getFullYear()}`
}

// A template for a markdown Readme file
function readme({ name }: Context) {
  return `# Hello ${upperFirst(name)}

This is a readme generated by Pinion

${copyrightTemplate()}
`
}

export function generate(init: Context) {
  return Promise.resolve(init)
    .then((context) => {
      return {
        ...context,
        name: 'david'
      }
    })
    // Render the readme template
    .then(renderTemplate(readme, toFile('readme.md')))
}
sh
npx pinion generators/readme.tpl.ts

Note

Note that any module used in a generator needs to be installed as a dependency. If you are not using it in your production project, it can be installed as a development dependency.

Injecting

Text and code can be injected into existing files at certain places using the inject task. This is very useful for wiring up newly generated files like components, middleware, controllers, services etc.

This is done by passing a template and an injection point to the inject task. The available injection points are:

  • before(text|context => text) inject before the first line that contains text
  • after(text|context => text) inject after the first line that contains text
  • prepend() inject at the beginning of the file
  • append() inject at the end of the file

The following example generates a TypeScript Express middleware and imports and registers it in src/app.ts:

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

interface Context extends PinionContext {
  name: string
}

function importTemplate({ name }: Context) {
  return `import { ${name} } from './middleware/${name}.js'`
}

const registerTemplate = ({ name }: Context) => `app.use('/${name}', ${name})`

function middlewareTemplate({ name }: Context) {
  return `import { Request, Response, NextFunction } from 'express'

export const ${name} = (req: Request, res: Response, next: NextFunction) => {
  console.log(\`Hello from ${name} middleware\`)
  next()
}`
}

export function generate(init: PinionContext) {
  return Promise.resolve(init)
    .then(
      prompt({
        name: {
          type: 'input',
          message: 'What is the name of your middleware?'
        }
      })
    )
    // Render the middleware template
    .then(
      renderTemplate(
        middlewareTemplate,
        toFile(({ name }) => ['src', 'middleware', `${name}.ts`])
      )
    )
    .then(inject(importTemplate, prepend(), toFile('src', 'app.ts')))
    .then(
      inject(
        registerTemplate,
        after('const app = express()'),
        toFile('src', 'app.ts')
      )
    )
}
ts
import express from 'express'

const app = express()

export { app }
sh
npx pinion generators/middleware.tpl.ts

What's next

We have seen the prompt function a few times already. The User Input chapter will go more into depth how to ask questsion and handle command line arguments. For more information on the tasks and helpers used in this chapter, see the API documentation.