Custom File Generator Tutorial
Seth Davis
Posted on September 29, 2023
As a developer who works on multiple React projects daily, I like having a tool that can help me quickly and efficiently write consistent code. One of the best ways I've found is writing a custom command line tool to rapidly scaffold out my most common code patterns.
My tool of choice is Plop.js. Plop is a powerful "micro-generator framework" built to help maintain patterns as well as speed up your project build time. From the documenation:
If you boil plop down to its core, it is basically glue code between inquirer prompts and handlebar templates.
In this tutorial, we'll build out a simple React component generator for your Typescript projects. By the end, you'll have a fully functioning CLI that is customized to your file generating needs. Let's get started.
Prerequisites
You must have node
& npm
installed. For more information, visit https://nodejs.org/
Setup
Here's the funnest part, you get to name your CLI! I'm going to call mine jarvis
.
Start by creating a directory and changing into that directory:
mkdir jarvis && cd jarvis
Initialize git repo:
git init
Ignore node_modules
:
echo "node_modules" > .gitignore
Add a README.md
:
echo "# Jarvis CLI" > README.md
Initialize a package.json file:
npm init -y
Modify package.json
Add a bin
key. This will be the command line tool's name:
"bin": {
"jarvis": "./index.js"
},
Change the scripts section to include a plop
script:
"scripts": {
"plop": "plop"
},
Install plop
npm i -D plop
Create the index file where our plop setup is going to live:
touch index.js
Add Plop CLI instructions (source) by copying the following code into your index.js
file:
#!/usr/bin/env node
import path from 'node:path';
import minimist from 'minimist';
import { Plop, run } from 'plop';
const args = process.argv.slice(2);
const argv = minimist(args);
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const config = {
cwd: argv.cwd,
configPath: path.join(__dirname, 'plopfile.js'),
preload: argv.preload || [],
completion: argv.completion
};
const callback = (env) => Plop.execute(env, run);
Plop.prepare(config, callback);
The main take away from the code above is that Plop will launch in the current working directory and it will allow you pass parameters to your command line tool (more on this later).
Time to make a plopfile - for the next few steps, I'm going to take a more granular approach. Each step will add to the same chunk of code which will end up being the full plopfile
code. Let's make the file:
touch plopfile.js
For starters, you'll need to export a default function with plop
as an argument. In plopfile.js
, add:
export default function(plop) {
// ...
}
Plop has a method called setGenerator
. It takes an array of prompts
and an array actions
. Based on the answers from the prompts
you will get a customized output. Here we'll create our first generator, called ts-component
:
export default function (plop) {
plop.setGenerator('ts-component', {
description: 'A React component written in Typescript',
prompts: [],
actions: []
});
}
Remember when inquirer
was mentioned earlier? Here's where we'll use our inquirer prompt types. We'll be needing a name
for our component, so add an input
prompt to our array of prompts, like this:
export default function (plop) {
plop.setGenerator('ts-component', {
description: 'A React component written in Typescript',
prompts: [
{
type: 'input',
name: 'name',
message: 'Component name'
}
],
actions: []
});
}
Next, we need to add an action. Plop has a few to choose from and for this tutorial, we'll be using the addMany
action. As the names suggests, it will add multiple files to the destination
that we give it.
Add an object to the actions array, give it a type
of addMany
. For destination
we'll use ${process.cwd()}/{{ pascalCase name }}
. This little string will point to the folder where the command was executed and create a folder with the name
of your component in pascal case. The next key is the templateFiles
which we have not created yet, go ahead and add it anyway. Lastly is the base
key which chops the namespace to whatever you like (see here for more).
Your plopfile should now look like this:
export default function (plop) {
plop.setGenerator('ts-component', {
description: 'A React component written in Typescript',
prompts: [
{
type: 'input',
name: 'name',
message: 'Component name'
}
],
actions: [
{
type: 'addMany',
destination: `${process.cwd()}/{{ pascalCase name }}`,
templateFiles: 'plop-templates/ts-component',
base: 'plop-templates/ts-component'
}
]
});
}
Make plop-templates
Here is where all of our templats are going to live. From the root
, run:
mkdir plop-templates
I personally like having a folder for each of my generators. That way when I use addMany
I can have the entire folder "copied" instead of individually adding single files with the add
action.
Inside of the plop-templates
directory, make a ts-component
directory:
mkdir ts-component
Let's make some ts-component template files. While in the ts-component
directory, run:
touch index.ts.hbs
Here's our first usage of the handlebars
(docs) template. The .hbs
extension will be removed once we run the action. Inside index.ts.hbs
, add:
export { default } from "./{{ pascalCase name }}";
This file is more of a personal preference, sometimes called a "barrel" file. It allows you to write imports a little cleaner. Instead of having ../components/Button/Button
be your import, you can just write ../components/Button
.
Next we're going to use a funky file name that plop will use to create our unique file name:
touch "{{ pascalCase name }}.tsx.hbs"
In the {{ pascalCase name }}.tsx.hbs
, let's drop some Handlbar syntax into our component template file:
import React from 'react';
export default function {{ pascalCase name }}({ children }): JSX.Element {
return <div>{children}</div>;
}
Again, you'll see pascalCase
added. That's a plop case modifier - I recommend checking them out if you have different preferences.
If you'd like to see example repo, feel free to copy/clone this example.
Link it up
We are now at the point where we can link to npm globally. In the root of jarvis
run:
npm link
If all goes well you should see a symlink get added to your computer and now you should be able to run jarvis
.
You can use the jarvis
command as is, it will prompt you for which generator (assuming you have more than one) OR you can pass args to jarvis
in order to execute the generator instantly.
For the ts-component
generator, the pseudo syntax would be:
<cli-name> <generator-name> <component-name>
Example:
jarvis ts-component Button
Tada! Button is ready to go!
Alternative uses
Now that jarvis
is working, think about the other file types you write daily. You don't just have to write Typescript files...Are you a blogger? Make a markdown template for your posts. Are you a Python dev? Have jarvis
make you a script template.
The possibilities are limitless!
Conclusion
Congrats on making your custom file generator. I hope this tutorial helps you in your day-to-day. Reach out if you have any questions.
Happy hacking :)
Posted on September 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.