Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CiceroLino/d96e1bc0adec60abafaa5a087871833a to your computer and use it in GitHub Desktop.
Save CiceroLino/d96e1bc0adec60abafaa5a087871833a to your computer and use it in GitHub Desktop.
Custom File Generator CLI Tutorial

Custom File Generator CLI Tutorial

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 a 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 and unit test 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 and unit test 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 and unit test 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

Make ts-component template files. While in the ts-component directory, run:

touch index.ts.hbs

Here's our first usage of the handlebars template. .hbs 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';

const {{ pascalCase name }}: React.FunctionComponent = ({ children }) => {
    return <div>{children}</div>;
};

export default {{ pascalCase name }};

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

Let's test it in an app

To do this, we will quickly setup a Create React App in Typsescript. Use this command wherever you store your projects:

npx create-react-app jarvis-sandbox --template typescript --use-npm

Navigate to the src folder within jarvis-sandbox. Create a components folder, change into that directory. Run jarvis - you should now see a prompt for your first generator!

Fill out the answers, then you should see an output similar to this:

~/Desktop/jarvis-sandbox/src/components master
❯ jarvis
? Component name Button
✔  +! 2 files added
 -> -sandbox/src/components/Button/index.ts
 -> -sandbox/src/components/Button/Button.tsx

Import your newly added component and you're ready to go!

Conclusion

Congrats on making your custom file generator. Want to take it a step further? Make more generators. Make more prompts to make your templates even more customizable. Tack on some unit test templating. Heck, you could even have a generator that outputs entire React web app. The choice is up to you.

Example repo

Hope you enjoyed this tutorial and happy hacking :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment