Scaffolding & boilerplate code generation with Plop

Scaffolding & boilerplate code generation with Plop

ยท

8 min read

I write this blog knowing that right now everyone is fanning out on AI (Generative Pre-trained Transformer 3) for any automation around code, and that is great! If you would prefer to look into this topic more, I suggest starting with #openGPT on Twitter or OpenAI Turn comments into code & API example.

Plop - Looks to be quite popular for React file generation and is better suited to smaller file code generation tasks, But it can be used to generate entire apps.

Other considerations

Yeoman looks better suited to generating entire apps but can also generate files and is cross-cutting so your backend and your frontend can use the same code generator.
Yeoman would be my next recommendation if Plop doesn't quite fit your needs.๐Ÿ‘

Let me know if you would like to know more about the above-mentioned considerations, I can write another blog about them. ๐Ÿ™Œ

Things to consider before deciding on any automated code generation.

At what level do you want to set conventions and maintain code generation? App folder level or the file level?

  • Generating an entire app, all or part of scaffolding is required to get developing quickly, but would also be used infrequently.

  • Files/folders in existing apps File code generation will have a high frequency of use for feature work and adding to existing apps, which could add the highest value.

Consider your current codebase, which provides the most value?

Does the answer lend itself to maintaining conventions & patterns for app folder scaffolding, file boilerplate or maybe both?

Frequent use is not always a great indication of the high value of auto-generating code. Through frequent implementation, consistency is higher, but if it is the same implementation over and over, we should automate it.
Infrequent implementation can be less consistent, meaning we should lean on a template for consistency.

My recommendation; When starting with maintaining automated code generation templates, start at the file level. It is more forgiving, quicker to implement and easier to update ๐Ÿ’ช

The below examples only show how to generate new code, not how to update existing code.


Plop simple setup

package.json

{
    "name": "plopCodeGen",
    "version": "1.0.0",
    "description": "collection of templates for generating file boilerplate or app skeletons",
    "main": "plopfile.js",
    "author": "me@hello.com",
    "private": true,
    "scripts": {
        "new": "plop",
        "new:app": "plop app",
        "new:comp": "plop component",
        },
    "devDependencies": {
        "plop": "^3.1.1",
    }
}

plopfile.js

You will notice the first arg of the plop.setGenerator(name, config) matches the scripts in the package.json

import generateApp from './templates/appTemplate'
import generateComponent from './templates/componentTemplate'

module.export = function (plop) {
    plop.setGenerator('app', generateApp),
    plop.setGenerator('component', generateComponent)
}

Templates

Example; App skeleton - simple folders & files layout.

templates/
    appTemplate/
    |   app/
    |   |   public/
    |   |   |   favicon.ico
    |   |   |   index.html.hbs
    |   |   |   manifest.json.hbs
    |   |   src/
    |   |   |   App.js.hbs
    |   |   |   index.css.hbs
    |   |   |   index.js.hbs
    |   |   |   routes.js.hbs
    |   |   test/
    |   |   package.json.hbs
    |   index.js
    |

Generate an app skeleton

Prompts and actions

In actions you will see to get our app folder structure correct we have to replicate the type: 'add', rather than use the type: addMany which will add many files into one folder. There are many action types you can use.

You can also see the "name" variable has a modifier in front of it pascalCase you can find all modifiers here.

templates/appTemplate/index.js

module.exports = {
  description: 'Generates new App folder',
    prompts: [
      {
// "input" standard inquirer prompt
        type: "input",
// name is the variable you can reuse in your template files and path
        name: "name",
// message is the prompt to the user aka. context
        message: "What's the name of the new app?",
// optional input validation function
        validate: function (value) {
          let message = true
          if (!/.+/.test(value)) {
            message = console.error('Missing', 'you must define the app name')
          } else if (value.length < 4) {
            // send new message for user feedbaack 
            message = console.error(
              'Too Short',
              `"${value}" is not descriptive enough`,
            )
          }

          return message
        }
      }
    ],
// TODO:: Make destination path an input in above prompts for user to choose where the component should live.
  actions: function () {
    return [
      {
        // action type
        type: 'add',
        // output path
        path: '../../../apps/{{pascalCase name}}/package.json',
        // input path
        templateFile: './templates/appTemplate/package.json.hbs',
        // the section of the path that should be excluded when adding files to the destination folder
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/src/App.js',
        templateFile: './templates/appTemplate/src/App.js.hbs',
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/src/index.css',
        templateFile: './templates/appTemplate/src/index.css.hbs',
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/src/index.js',
        templateFile: './templates/appTemplate/src/index.js.hbs',
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/src/routes.js',
        templateFile: './templates/appTemplate/src/routes.js.hbs',
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/public/favicon.ico',
        templateFile: './templates/appTemplate/public/favicon.ico',
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/public/index.html',
        templateFile: './templates/appTemplate/public/index.html.hbs',
        base: 'app'
      },
      {
        type: 'add',
        path: '../../../apps/{{pascalCase name}}/public/manifest.json',
        templateFile: './templates/appTemplate/public/manifest.json.hbs',
        base: 'app'
      },
    ]
  }
}

index.js.hbs

import React, { StrictMode } from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
import { BrowserRouter } from "react-router-dom"

import './index.css'

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>
)

App.js.hbs

import React from 'react'
import Routes from './routes'

const App = () => <Routes />

export default App

routes.js.hbs

import { Routes, Route } from 'react-router-dom'

export default function {{pascalCase name}}AppRoutes() {
    return (
      <Routes>
        <Route path="/" element={<Landing />} 
          <Route path="/tab-one" element={<TabOne />} />
          <Route path="/tab-two" element={<TabTwo />} />
        </Route>
      </Routes>
    )
}

index.css.hbs

This is a small preview of a main.css file showing that you can add your CSS as normal you could ask in your prompts for global styling named variables if you wanted to add them here.

body,
html {
    height: 100%;
    margin: 0;
    padding: 0;
    font-family: ...
}

manifest.json.hbs

This is a small preview of a manifest file showing how you can assign the name variable to keys in the JSON file.

{
    "short_name": "{{name}}",
    "name": "The Best {{name}}",
    "icons": [],
    ...
}

Generate a file

Prompts and actions

templates/componentTemplate/index.js

module.exports = {
  description: 'Generates new component',
  prompts: [
    {
      type: 'input',
      name: 'name',
      message: "What's the name of the component? (make sure to suffix/prefix with ...)",
      validate: function (value) {
        let message = true

        if (!/.+/.test(value)) {
          message = console.error('Missing', 'you must define a component name')
        } else if (value.length < 4) {
          message = console.error(
            'Too Short',
            `"${value}" is not descriptive enough`,
          )
        }

        return message
      },
    },
    {
      type: 'input',
      name: 'app',
// useful for multi app codebase or in not multi just add src/pages/app or remove.
      message: "What's the name of the app for this component to be added?",
      validate: function (value) {
        let message = true

        if (!/.+/.test(value)) {
          message = console.error('Missing', 'you must define a app name')
// TODO:: add some smarts, can a config file be used to check the existence of an app here?
        } else if (value.length < 4) { 
          message = console.error(
            'Too Short',
            `"${value}" is not descriptive enough`,
          )
        }

        return message
      }
    },
    {
      type: 'input',
      name: 'location',
      message: "Where inside the app src folder should this component live? (e.g. shared common folder)",
      validate: function (value) {
        let message = true

        if (!/.+/.test(value)) {
          message = console.error('Missing', 'you must define a folder location name')
// TODO:: add some smarts, can a config file be used to check the existence of an app here?
        } else if (value.length < 4) {
          message = console.error(
            'Too Short',
            `"${value}" is not descriptive enough`,
          )
        }

        return message
      }
    }
  ],
// TODO:: make destination path an input above for user to choose where the component should live.
  actions: function () {
    return [
      {
        type: 'add',
        path: '../../../apps/{{app}}/src/{{location}}/{{pascalCase name}}/index.js',
        templateFile: './templates/componentTemplate/index.hbs',
      }
    ]
  }
}

index.js.hbs

import React from 'react'
// Add your other imports for this component here...

// Add types here if you are using .ts/.tsx

// NOTE: If you know the required props for this component you can destructure them and add them below insted of "props"
function {{pascalCase name}}(props) {
// Add functions as you need them...

// Add as usual add your component return jsx...
  return (
    <div className="">
      <label>HW</label>
    </div>
  )
}

Some Pros & Cons of using Plop

Pros:

  • Very quick implementation gentle learning curve.

  • Easy to add new code to existing apps.

  • Easy to make adjustments to templates to keep aligned with any new conventions or updated patterns*.

  • You can also import non-template files like favicon.ico...

Cons:

  • * Because of its ease of use, it is simple to make adjustments to the templates that suit the developer on the fly. Docs and an opinionated implementation guide would need to be maintained.

  • Plop template files cannot be dynamically downloaded and used from a template repo. They can be published as a (private) npm module, git issue


Some final things to consider

  • Is code auto-gen a complete buy-in for all devs when creating new apps/folders/files?

  • Does this implementation replace manual creation altogether? Should it?

  • Where/How should manual code creation be managed when used side by side with automated code auto-gen?

  • How can conventions and code patterns be established for templates?

  • How can existing auto-generated code be updated when the code auto-gen template changes with an evolving codebase?

I recommend using code auto-generation for brand new projects or files, but only after establishing some foundations & opinions on how templating should be implemented and maintained.
It is a great way for any developer, especially freelancers or solo workers to free up some time so you can get down to the fun stuff.

Till next time ๐Ÿ‘‹


Did you find this article valuable?

Support Hey Rory's blog by becoming a sponsor. Any amount is appreciated!

ย