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')
orfile(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:
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:
- copyFiles copies files from a source (usually static file) directory to a target
- loadJSON, writeJSON and mergeJSON to work with JSON files
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:
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
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 containstext
after(text|context => text)
inject after the first line that containstext
prepend()
inject at the beginning of the fileappend()
inject at the end of the file
The 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
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.