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:
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)
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
:
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')))
}
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:
import { PinionContext } from '@featherscloud/pinion'
interface Context extends PinionContext {
// ...
}
By extending PinionContext
, your Context
interface now has the following properties:
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 thegenerate
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 theContext
interface. - Line 22: the object returned in the promise adds a
year
property to the context. This makesyear
available to subsequent functions and to the template. TherenderTemplate
utility then calls the template function with the updatedcontext
. - Line 9: the
context
is received by the template function. - Line 14: the
year
property is used in the template.
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')))
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.
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
})
}
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.