Written by Joseph Mawa✏️
When working with certain design patterns, your project will almost always follow a specific structure. Manually setting up these projects can be daunting and tedious. You can save yourself time by using code generators for these repetitive tasks.
Code generators are tools you can use to automate repetitive tasks such as generating boilerplate code. They increase productivity by freeing up time so that you can focus on more productive areas of your project. A code generator can generate configuration files, create directories, and set up necessary tooling. These tools include linters, testing frameworks, compilers, and more.
There are several code generation tools in the JavaScript ecosystem, including Pinion, Yeoman, Plop, and Hygen. In this article, we will focus on the Pinion code generator toolkit and how to use it to automate repetitive tasks.
What is a code generator?
Generators remove low-level, tedious, repetitive tasks such as creating directories and setting up development tools like linters, testing frameworks, compilers, and transpilers so that you focus on more productive activities like building and shipping products. Code generators also ensure you follow best practices and consistent style and structure in your project, making your project easy to maintain.
Code generators typically depend on user preferences to ensure that the generated code aligns with the developer's needs. These preferences can be specified via the command line.
There are several third-party code generators developed using tools such as Yeoman. However, there is no one-size-fits-all code generator. The existing code generators may not meet your project requirements, coding style, or licensing requirements, especially when building enterprise projects. You also need to take into account the long-term maintenance and potential security issues such packages may pose.
To meet your project requirements, you might need to develop your own generator using tools like Yeoman, Hygen, Plop, or Pinion. In this article, we will focus on Pinion.
An introduction to Pinion
Pinion is a free, open source, MIT-licensed toolkit for creating code generators. It is a lightweight Node.js package. Though you need to use TypeScript to create code generators with Pinion, you can use Pinion in any project — including those that don't use Node.js or JavaScript.
If you want to use Pinion in a non-JavaScript or non-TypeScript project, you will need to first install the supported Node.js version and initialize the project using the npm init --yes
command before installing Pinion.
Pinion is type-safe, fast, composable, and has a minimal API. Therefore, you can learn it fairly quickly. But unlike other similar tools like Yeoman, Plop, and Hygen, Pinion is relatively new.
Using the Pinion code generator toolkit
In this section, you will learn how to use Pinion to automate repetitive tasks. Pinion is available in the npm package registry, so, you can install it like so:
npm install --save-dev @featherscloud/pinion
In the code above, we installed Pinion as a development dependency because you will almost always use code generators in a development environment.
With Pinion, you can create a code generator anywhere in your project and execute the generator from the command line. For example, if your code generator is in the generator/generate-blog-template.ts
file, you can execute it using the command below:
npx pinion generator/generate-blog-template.ts
Instead of typing the path to your code generator each time you want to run it, you can add a script with a meaningful name to your package.json
file as demonstrated in the example below. It will ensure that anybody contributing to your project can use your code generator without having to learn Pinion or even knowing what tool you're using:
{
...
"scripts": {
"generate-blog-template": "npx pinion generator/generate-blog-template.ts",
},
...
}
If you have the above script in your package.json
file, you can now execute it like so:
npm run generate-blog-template
A typical Pinion code generator has two primary components: a TypeScript interface that defines the context, and a generate
function that you must export from your generator. The generate
function wraps the context and renders the template.
The example below shows a basic generator that creates a basic blog post template. Notice how we use a template literal to declare the template:
import { PinionContext, toFile, renderTemplate } from "@featherscloud/pinion";
// An empty Context interface
interface Context extends PinionContext {}
// The file content as a template string
const blogPostTemplate = (): string =>
`# Blog title
## Introduction
## Conclusion
`;
const getFormattedDate = (): string => {
const date = new Date();
const yyyy = date.getFullYear();
const mm = date.getMonth().toString().padStart(2, "0");
const dd = date.getDate().toString().padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
};
// A function export that wraps the context and renders the template
export const generate = (init: Context) => {
return Promise.resolve(init).then(
renderTemplate(blogPostTemplate, toFile(`${getFormattedDate()}.md`))
);
};
Running the above generator will create a blog post template in Markdown format. It will use today's date in the yyyy-mm-dd.md
format to name the generated file. In a real-world project, you will most likely get the file’s name from the user.
How to create templates in Pinion
Unlike other code generators, Pinion doesn't have a separate templating language. It uses template strings (template literals) to create templates. A template string is a built-in feature of JavaScript and TypeScript. Therefore, it doesn't introduce learning overhead like code generators that use other templating languages.
You can create a template and write it to a file or inject it into an existing file. In the example below, I am creating a simple blog post template and rendering it to a new Markdown file. Notice how I'm dynamically adding the date to the front matter and using the date to create the file name:
import { PinionContext, toFile, renderTemplate } from "@featherscloud/pinion";
interface Context extends PinionContext {}
const getFormattedDate = (): string => {
const date = new Date();
const yyyy = date.getFullYear();
const mm = date.getMonth().toString().padStart(2, "0");
const dd = date.getDate().toString().padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
};
const blogPostTemplate = () => `---
layout: blog-post-layout.liquid
title: "Blog post 1"
date: ${getFormattedDate()}
tags: ['post']
---
## Introduction
Your introduction
## Conclusion
Your conclusion
`;
export const generate = (init: Context) => {
return Promise.resolve(init).then(
renderTemplate(blogPostTemplate, toFile(`${getFormattedDate()}.md`))
);
};
In the above example, I am using the toFile
helper function. The other file-related helper functions include file
and fromFile
. You can read about them in the Pinion documentation.
You can execute the above generator using the following command. It will create a new Markdown blog post template:
npx pinion path/to/generator/file.ts
How to handle user inputs
Sometimes, a static template isn't enough to automate repetitive tasks. To create a more tailored template, try adding user input for customization.
In Pinion, you can use the prompt
task to prompt user inputs from the command line. Internally, Pinion uses the Inquirer
package to receive user input.
Remember that in the previous section, we created a simple blog post template and used today's date to name it. Instead of always creating a template with the same filename, blog post title, and author, let's dynamically get this information from the user, as shown in the example below:
import {
PinionContext,
renderTemplate,
toFile,
prompt,
} from "@featherscloud/pinion";
interface Context extends PinionContext {
filename: string;
blogPostTitle: string;
author: string;
}
const getFormattedDate = (): string => {
const date = new Date();
const yyyy = date.getFullYear();
const mm = date.getMonth().toString().padStart(2, "0");
const dd = date.getDate().toString().padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
};
const blogPostTemplate = ({ blogPostTitle, author }: Context) => `---
layout: blog-post-layout.liquid
title: ${blogPostTitle}
author: ${author}
date: ${getFormattedDate()}
tags: ['post']
---
## Introduction
Your introduction
## Conclusion
Your conclusion
`;
export const generate = (init: Context) =>
Promise.resolve(init)
.then(
prompt((context) => {
return {
filename: {
type: "input",
message: "What is the filename?",
when: !context.filename,
},
blogPostTitle: {
type: "input",
message: "What is the title of your blog post?",
when: !context.blogPostTitle,
},
author: {
type: "input",
message: "What is the name of the author?",
when: !context.author,
},
};
})
)
.then(
renderTemplate(
blogPostTemplate,
toFile(({ filename }) => [
"posts",
`${filename}-${getFormattedDate()}.md`,
])
)
);
When you execute the above generator as before, you will be prompted for the filename, blog post title, and the author’s name before creating the blog post template.
How to compose code generators
One of the benefits of using Pinion is that the generators you create are composable. For example, you may have a code generator for creating a project README template, a separate one for creating a project code of conduct template, and a third for creating a project license.
You can compose the three generators in another generator. In the example below, I am composing the three in a generator that initializes an npm project:
import { PinionContext, exec, prompt } from "@featherscloud/pinion";
import { generate as generateCodeOfConduct } from "./code-of-conduct";
import { generate as generateLicense } from "./license";
import { generate as generateReadme } from "./readme";
interface Context extends PinionContext {
projectName: string;
}
export const generate = (init: Context) => {
return Promise.resolve(init)
.then(
prompt((context) => {
return {
projectName: {
type: "input",
message: "What is your project name?",
when: !context.projectName,
},
};
})
)
.then(generateReadme)
.then(generateLicense)
.then(generateCodeOfConduct)
.then((ctx) => ({ ...ctx, cwd: `${ctx.cwd}/${ctx.projectName}` }))
.then(exec("npm", ["init", "--yes"]))
.then((ctx) => {
return { ...ctx, cwd: ctx.pinion.cwd };
});
};
The above generator will prompt you for the project name. It will then create a project README, a license, and a code of conduct file in Markdown format before initializing the directory as an npm project.
Comparing Pinion with other code generator tools
Pinion is a relatively new code generator toolkit in the JavaScript ecosystem. In this section, we will compare Pinion and some more mature and battle-tested code generation tools.
Though our focus will primarily be on their features, we will also highlight their npm download statistics, GitHub stars, issues (open and closed), and maintenance. Keep in mind that a high star count doesn't guarantee that a package is secure, feature-rich, or of better quality than the others. Additionally, metrics can change over time, and an actively maintained project today may become unmaintained in a few months. You should thoroughly investigate before using a package in an enterprise project.
One of the benefits of using Pinion over other code generation tools like Hygen, Yeoman, and Plop is its templating language. With Pinion, you use plain JavaScript/TypeScript template strings to write templates. If you know JavaScript/Typescript, then you already know Pinion's templating language; there is no learning curve.
A typical Pinion template is a template string that looks like the following:
const readme = ({ organization }) =>
`# ${organization}
This is a readme
Copyright © 2024 ${organization} developers
`
Other code generation tools use third-party templating languages. For example, Plop uses handlebars and Hygen uses EJS. These templating languages have their own syntax. Therefore, unless you're already familiar with their syntax, they introduce a learning curve.
Assuming you want to use Hygen instead of Pinion, you will have to translate the above template to EJS so that it looks like this:
---
to: app/readme.md
---
# <%= organization %>
This is a readme
Copyright © 2024 <%= organization %> developers
The above code may look simple for basic “Hello, World!” templates, but you need an intermediate to advanced level of familiarity with EJS to write more complex templates.
Furthermore, as highlighted above, Pinion forces you to use TypeScript when writing code generators; TypeScript introduces type safety that reduces hard-to-debug errors. The other tools do not have this benefit.
Yeoman is one of the most popular and battle-tested code generation tools in the JavaScript ecosystem. It has a whole ecosystem of tools and code generators developed by the community. Therefore, if you are looking to build a code generator using Yeoman, there is a chance that someone has built a similar Yeoman code generator.
On the other hand, Pinion, as well as Hygen, and Plop do not have an extensive collection of community-built code generators like Yeoman. You will almost always have to bootstrap your code generator from scratch. However, they’re much easier to pick up than Yeoman:
Pinion | Yeoman | Hygen | Plop | |
---|---|---|---|---|
Templating language | Typestring template string | Handlebars, EJS, Jade | EJS | Handlebars |
Active maintenance | Yes | Yes | Yes | Yes |
Documentation | Good | Good | Good | Good |
Pricing | Free | Free | Free | Free |
Community support | Good | Good | Good | Good |
Open GitHub issues | 3 | 97 | 79 | 43 |
Closed GitHub issues | 3 | 491 | 192 | 248 |
Conclusion
Code generator toolkits like Pinion are useful for automating repetitive tasks like creating a new project that follows a specific design pattern. By eliminating the more mundane development tasks, these tools help increase your development speed and reduce the time to production.
Pinion distinguishes itself as a fast, flexible, and type-safe toolkit, licensed under MIT and developed in TypeScript. It supports creating and executing code generators in TypeScript, using built-in JavaScript/TypeScript template string for its templating language.
LogRocket: Debug JavaScript errors more easily by understanding the context
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Top comments (0)