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.
There are three main helpers for working with file pointers:
file('readme.md')
, file('docs', 'readme.md')
or file(context => [context.docsPath, 'readme.md'])
file
but makes sure that the file already existsfile
but will create the file including missing directories if it does not existThe 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:
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'])
)
)
}
npx pinion generators/readme.tpl.ts
The built-in file related tasks are:
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:
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')))
}
npx pinion generators/readme.tpl.ts
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.
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 fileappend()
inject at the end of the fileThe following example generates a TypeScript Express middleware and imports and registers it in src/app.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')
)
)
}
import express from 'express'
const app = express()
export { app }
npx pinion generators/middleware.tpl.ts
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.