Creating a CLI to Automate File Creation

m0nica

Monica Powell

Posted on March 7, 2020

Creating a CLI to Automate File Creation

Do you ever find yourself copying and pasting the same boilerplate code for multiple files at a time? Do you stop and think every time you have to construct an ISO 8601 formatted date? 🤔
How frustrating would it be if you could no longer copy and paste your template code from one file to another?

This article will walk through how to quickly create a command-line interface (CLI) tool that generates text-based files. In particular, the examples in this article will walk through how to create templates to generate a new .jsx Page in a Gatsby blog with tests as well as how to generate markdown files with the initial structure of a blog post. These examples should serve as inspiration as the sky is the limit in regards to what type of text files can be generated based on the needs of a particular developer or project.

Why Scaffolding?

The CLI we will be creating is a type of scaffolding software as it generates starter code based on templates. Note that starter code generated by scaffolding generally is not ready for production however it still has benefits as it can improve the developer experience, make it easier to implement standardization and enable faster software delivery.

Some of the benefits of scaffolding can be:

  • involving less work - no more copying and pasting boilerplate code (i.e., relative imports from one file to another)
  • automating the implementation of design patterns and best practices
  • reducing time to generate new projects or components
  • being less error-prone than the manual process of copy & pasting & editing
  • encouraging consistency and implementation of design patterns

Scaffolding can answer questions like:

  • Where should translations for this React component live?
  • Where can I find our current code standards?
  • What directory should this type of file live in?
  • Should I use cameCase? snake_case? kebab-case? PascalCase? UPPERCASE_SNAKE_CASE?

Is scaffolding "worth it"?

Implementing scaffolding takes time. The potential benefits of scaffolding a particular piece of software versus time involved in developing it should be assessed to determine if it's worth the time and effort to implement scaffolding. If we are analyzing the estimated time invested vs. saved and not other intangible benefits like consistency or reducing context switching you can use the below comic to assess if it's worth implementing.

is it worth the time comic

In terms of other potential down-sides, code requirements often evolve over time and scaffolded templates may require future maintenance as new requirements surface. Ideally refactoring the scaffolding template should feel like a natural extension of a workflow versus like maintenance is additional overhead and slowing down the process. Some implementation details or decisions can be concealed by scaffolding which can reduce context depending on the tool the actual logic being used to generate files can be easily accessible.

Micro-generator tool: PlopJS

If you’re looking for a lightweight way to introduce scaffolding into your workflow consider using Plop, a micro-generator. Plop allows developers to generate files based on user input via a Command Line Interface (CLI) with minimal setup.

How does Plop work?

PlopJS combines the Handlebars templating language and Inquirer.js. Inquirer.js is a tool for collecting user input via CLI. You can present questions a.k.a CLI prompts in different formats with inquirer. Handlebars is a templating language that you may be familiar with. Templating languages are used in a variety of contexts from displaying React props, creating e-mail templates, or even making your workflow easier as we'll see today. Before, I was using .jsx in React I worked with the Liquid templating language in Jekyll and Handlebars in Foundation for Emails.

By combining the functionality of Inquirer.js with Handlebars, plop allows users to quickly create templates with minimal setup. If you're not familiar with software templates you can think of them as similar to mail merge in a word processor. In mail merge, there's generally a spreadsheet with data which is and then merged with a template document that has placeholder variables. When the data and template are combined with mail merge the result is a version of the document that contains data in the proper places (as determined by the placeholder variables). The data in that file is populated during the mail merge process and customized as appropriate for the recipient. In our case, the data entered in the CLI will be populated into the template and generate a new file when we run plop.

Plop Set Up

If you already have a directory with a package.json where you want to generate files then Plop can be installed with yarn with the following command:

yarn add -D plop

or npm using:

 npm install —save-dev plop

If you don't already have a package.json you can create one by typing yarn init or npm init and walking through the steps and then installing plop.

Once plop is installed as a package dependency we should update the scripts in the package.json file to enable us to run yarn plop or npm plop to run plop. You can name "plop" whatever you want the command to be for example "generate": "plop" and it will behave the same way.

"scripts": {
 "plop": "plop"
}

Unlike code snippets, plop doesn’t require additional setup to share between computers or across developers and lives in version control. Also, plop allows multiple files to be generated at once.

Now we can create our first plopfile.js in the root level of our directory which is where the plop magic will happen.

plopfile.js:

module.exports = function(plop) {
  /* welcome messag that will display in CLI */
  plop.setWelcomeMessage(
    "Welcome to plop! What type of file would you like to generate?"
  ),
    /* name and description of our template */
    plop.setGenerator("generate blog post ✏️", {
      description: "Template for generating blog posts",

      prompts: [
        /* inquirer prompts */
        /* questions we want to ask in CLI and save questions for*/
      ],

      actions: [
        /* what should be generated based off of the above prompts */
      ],
    })
}

Now that we have a baseline plopfile.js let's add some functionality. First we will add the ability to generate the frontmatter or metadata that needs to appear on every draft blog post in order for Gatsby to properly generate it.

sample frontmatter:

---
title: Automating File Creation With JavaScript
date: 2020-01-14T12:40:44.608Z
template: "post"
draft: true
slug: 2020-01-14-automating-file-creation-with-javascript
category:
  - tutorial
description: This article walks through how to use plop a micro-generator to generate new text-based files.
---

We should update the plop file to add built-in functionality to format today's date into an ISOString and use Inquirer (built into Plop) to create CLI prompts and collect input.
We will use new Date(Date.now()) to get the current date. We will format the date both as an ISOStringDate: 2020-01-14T12:40:44.608Z and as a shortDate: 2020-01-14. The ISOStringDate
will be used in the frontmatter whereas theshortDatewill be used in the file path of the newly generated file. The date utils will be returned bysetplop.setHelper()in order to expose the values in our.hbstemplates by writing{{ISOStringDate}}or{{shortDate}}.

In terms of collecting input in prompts the basic structure of a prompt is

{
  // example inquirer types:
  // input, list, raw list, expandable list, checkbox, password, editor
  // learn more here: https://github.com/SBoudrias/Inquirer.js#prompt-types
  type: "input",

  name: "description",

  message: "Description of post:",

  }

The most complex prompt in this example is this list prompt which allows users to use arrow keys to select the category for their blog post and then we transform that value to a lowercase string. The filter prompt can be used to convert a user-friendly value like "yellow" to being inserted in the template as #ffff00.

 {
          type: "list",
          name: "category",
          message: "Category:",
          choices: ["Tutorial", "Reflection"],
          filter: function(val) {
            return val.toLowerCase()
          },
        },

Once all of the prompts are squared away we need to do something with the input by adding an action:

{
          type: "add",
          path: `content/blog/${shortDate}-{{dashCase title}}.md`,
          templateFile: "src/plop-templates/blog-post.hbs",
        },

A type of action add creates a new file at the path and interpolates the responses from the prompts and values from plop helpers into the templateFile.

The complete plopfile.js at this point should look something like this:

module.exports = function(plop) {
  // highlight-start
  const today = new Date(Date.now())
  const shortDate = today.toISOString().split("T")[0]
  plop.setHelper("shortDate", () => shortDate),
    plop.setHelper("ISOStringDate", () => today.toISOString()),
    // optional welcome message

    // highlight-end
    plop.setWelcomeMessage(
      "Welcome to plop! What type of file would you like to generate?"
    ),
    plop.setGenerator("blog post ✏️", {
      // highlight-start
      description: "template for generating blog posts",
      prompts: [
        {
          type: "input",
          name: "title",
          message: "Title of post:",
        },
        {
          type: "input",
          name: "description",
          message: "Description of post:",
        },

        {
          type: "list",
          name: "category",
          message: "Category:",
          choices: ["Tutorial", "Reflection"],
          filter: function(val) {
            return val.toLowerCase()
          },
        },
      ],
      actions: [
        {
          type: "add",
          path: `content/blog/${shortDate}-{{dashCase title}}.md`,
          templateFile: "src/plop-templates/blog-post.hbs",
        },
      ],
      // highlight-end
    })
}

In order to actually use this we need to create the blog-post.hbs template in our src/plop-templates/ directory. This .hbs file is where we parametrize the code to only keep the bits that we need from file to file and to have placeholders for things that change based on the name or type of thing that is being generated. Plop has built-in case helpers like titleCase or dashCase to format input (view the built-in case modifiers at: https://plopjs.com/documentation/#case-modifiers)

blog-post.hbs

---

title: {{titleCase title}} # from title prompt

date: {{ISOStringDate}} # from plopHelper

template: “post”

draft: true

slug: {{shortDate}}-{{dashCase title}} # from plop helper and title prompt

category:

- {{category}} # from category prompt

description: {{description}} # from description prompt

---
## Intro
{{description}}
<!— The blog post starts here >

Running yarn plop now should walk you through the prompts we added and generate a new file based off of the responses to the prompts and the handlebars template. The file that was generated will
be at content/blog/${shortDate}-{{dashCase title}}.md (or wherever you set the path in the action).

Plop Example to Generate JSX Page

Below is an update plopfile and example handlebars template for generating a Page.jsx and Page.test.jsx:

Page.hbs:

import React from "react"



// Components

import { Helmet } from "react-helmet"

import { graphql } from "gatsby"

import Layout from "../components/page/layout"



const {{properCase pageName}} = ({

data: {

site: {

siteMetadata: { title },

},

},}) => (<Layout>

<div>

<Helmet title={title} />

</div>

</Layout>)





export default {{properCase pageName}}



export const pageQuery = graphql`

query {

site {

siteMetadata {

title

}

}

}

pageTest.hbs:

import React from "react"

import { shallow } from "enzyme"

import Layout from "../components/page/layout"

import {{properCase pageName}} from "./{{properCase pageName}}"

import { Helmet } from "react-helmet"



const data = {

site: {

siteMetadata: {

title: “monica*dev”,

},

}

}



describe(“{{properCase pageName}}”, () => {

const component = shallow(

<{{properCase pageName}} data={data} />)



it(“renders page layout”, () => {

expect(component.find(Layout)).toHaveLength(1)

})



it(“renders helmet with site title from site metadata”, () => {

expect(component.find(Helmet).props().title).toBe(“monica*dev”)

})

})

plopfile.js

module.exports = function(plop) {

const today = new Date(Date.now())

const shortDate = today.toISOString().split("T")[0]

plop.setHelper("shortDate", () => shortDate),

plop.setHelper("ISOStringDate", () => today.toISOString()),

plop.setGenerator("generate blog post ✏️", {

 /*...*/

}),

plop.setGenerator("Create new page 📃", {

description: "template for creating a new page",

prompts: [

{

type: "input",

name: "pageName",

message: "Page name:",

},

],

actions: [

{

type: add,

path: src/pages/{{properCase pageName}}.jsx,

templateFile: src/plop-templates/page.hbs,

},

{

type: add,

path: src/pages/{{camelCase pageName}}.test.jsx,

templateFile: src/plop-templates/pageTest.hbs,

},

],

})

}

Formatting Output

I ran into an issue where because the initial template files were .hbs files the generated files weren't necessarily formatted like .md or .jsx. I already had prettier set up in my project so in order to solve the formatting issues I ended up updating my plop script shorthand to do format all files after running plop. However, this should be refactored to only format the relevant, just generated files.

"scripts": {
  ...
  "format": "prettier —write \"**/*.{js,jsx,json,md}\"",
  "plop": “plop && yarn format”
}

Conclusion

As a recap, we used plop to generate boilerplate code that is shared across certain types of files. Ideally implementing some type of automation for file creation should reduce time to create functional files, be less error-prone than copy + pasting + editing, encourages consistency and implementation of design patterns.

Create Your Own Template

Some ideas for things to incorporate in templates:

  • Create different templates based on the type of React component (examples in React-Boilerplate's generators)
  • Generate comments or base-level documentation
  • Generate self-contained directories or packages with, index file (and related tests), package.json and README.md

Additional Resources

Last year, I had the opportunity to streamline the creation of new packages via CLI prompts (which inspired my talk on generating React components at React Girls Conf in London) and led me to learn more about Plop. If you're interested in learning more about Plop in a React context or alternatives to Plop check out my previous talk.

Sketchnotes from Monica's Automating React Workflow Talk at React Girls Conf in London
Sketchnotes by @malweene from my talk at React Girls Conf

Here are some additional resources that may be helpful as you are getting more familiar with generating files with Plop.

This article was originally published on www.aboutmonica.com

💖 💪 🙅 🚩
m0nica
Monica Powell

Posted on March 7, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related