Skip to content

Instantly share code, notes, and snippets.

@sethdavis512
Last active October 25, 2023 10:51
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sethdavis512/0d1e2b93019373049e80d0e1f52a9d5c to your computer and use it in GitHub Desktop.
Save sethdavis512/0d1e2b93019373049e80d0e1f52a9d5c to your computer and use it in GitHub Desktop.
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 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 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

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 for 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 :)

@danya0365
Copy link

You saved my life 👍

@sethdavis512
Copy link
Author

You saved my life 👍

Wait - really?! That is so awesome. Thanks for the feedback @danya0365 - glad it was helpful.
Did you hit any issues or roadblocks? Anything that I should update or change?

@danya0365
Copy link

danya0365 commented Sep 17, 2023

You saved my life 👍

Wait - really?! That is so awesome. Thanks for the feedback @danya0365 - glad it was helpful. Did you hit any issues or roadblocks? Anything that I should update or change?

It's reduce my times to create CRUD backend and client side for 20 database tables just only in 1 command

for api

? [PLOP] Please choose a generator. module-server-use-case-with-supabase - generate module-server-use-case-with-supabase with name and module name ? Use case group name user ? Use case group names in pluralize users ? Database name users ? Module name admin ✔ +! 18 files added -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/presentation/api/create-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/presentation/api/delete-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/presentation/api/get-latest-users.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/presentation/api/get-user-by-id.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/presentation/api/get-users.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/presentation/api/update-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/api/admin/users/route.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/data/data-source/server/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/data/repository/server/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/data/type/db/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/helper/server/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/use-case/server/create-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/use-case/server/delete-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/use-case/server/get-latest-users.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/use-case/server/get-user-by-id.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/use-case/server/get-users.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/domain/use-case/server/update-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/admin/api/admin/users/[userId]/route.ts

for client

? [PLOP] Please choose a generator. module-client-use-case - generate module-client-use-case with name and module name ? Use case group name user ? Use case group names in pluralize users ? Module name user ✔ +! 10 files added -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/data/api/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/data/repository/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/model/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/transformers/user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/use-case/create-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/use-case/delete-user.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/use-case/get-latest-users.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/use-case/get-user-by-id.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/use-case/get-users.ts -> /Users/marosdeeuma/Documents/งานหลัก/tiny-game-nextjs/modules/user/domain/use-case/update-user.ts

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