Code Generator Neat Hack

Matti Bar-Zeev - Sep 16 '22 - - Dev Community

In this week’s short post I’d like to share with you a neat hack for the Plop code generator to overcome the “relative paths challenge”.

When it comes to software development, a good developer experience (or DX) is a key aspect in helping the development process to become faster and more efficient.
Having a good DX means that developers are focusing on the real deal while other, less important stuff, is being taken care of by automated tools.
Such a tool is Plop which I really love for its simplicity and efficiency.


Plop

Plop helps generate scaffold files for projects by presenting the developer a set of guiding questions and populating predefined templates with the input data.

In one of my previous article, called “Creating a React Component Generator”, I’ve described how you can use Plop to generate a React component in an existing project, which will include the component file, a css file for it, Jest test, Storybook story and a Cypress test, all just by giving the component name to the tool as input.

This is a very powerful tool which can help medium and large RnD orgs to save development time while maintaining the same standard for all components.

The Relative Paths Challenge

This challenge is not only related to Plop but to any file/code generator out there. Here it is:

Say you have a monorepo and you would like to have a package generator that will create a new package and wire it up with all the common configurations the monorepo might have (you can read more about examples of such configurations here and here).

These shared configurations usually allow a package to inherit a common configuration from the project’s root and extend it if required. I guess you can already see where the problem is -

Plop is working with Handlebars templates (.hbs files) in which it registeres the content of the files, along with some dynamic data which came from the developer’s answer , but here we’re dealing with paths to the configuration files, and these paths depend on the location of the package directory within the project - this is not something we would like our developers to be handling, nor should they.

For example, if we take the components package under my Pedalboard monorepo and check it’s TS configuration file, you will see that it extends a base configuration ../../tsconfig.base.json:

{
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
       "module": "CommonJS",
       "outDir": "dist/cjs",
       "declarationDir": "dist/types",
       "jsx": "react"
   },
   "files": ["index.ts"],
   "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

But if we decide that our generator will allow us to create packages in nested directories (meaning, not only under “/packages” but “/packages/my/nested/package”) then the Plop generator will create a hard-coded path which is broken.

So how do we solve it? How can we create a generator which knows to generate relative paths?

The Solution

First of all we are counting on the fact that one of the questions the Plop generator “asks” the developer is “where does this package reside on the project?”.
The answer to this question comes in the format of a file path, and let’s assume that it registers to a variable called “targetPath” in Plop.

Now, for calculating the relative path we’re going to use a Handlebars Helper, which is a reusable code we can then use in any template we wish, and it will get executed with the arguments we will pass to it.

I will call our helper “relativePath” and register it to Plop. Here is its code:

const path = require('path');

plop.setHelper('relativePath', (from, to)=>{
   const fromPath = `${from}/package`;
   return path.relative(fromPath, to);
});
Enter fullscreen mode Exit fullscreen mode

This helper function gets 2 args -
from: It is the file path that would be used as base path.
to: It is the file path that would be used to find the relative path
This function will return a string with the normalized form of the resolved path using the path.relative API.
(I’m using a fake package name called “package” here since we need to simulate a nested path of a package. The name is really not important here)

Let’s see how we actually use it in our Plop’s handlebars template:

{
   "extends": "{{relativePath targetPath ./tsconfig.base.json}}",
   "compilerOptions": {
       "module": "CommonJS",
       "outDir": "dist/cjs",
       "declarationDir": "dist/types",
       "jsx": "react"
   },
   "files": ["index.ts"],
   "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

We call the relativePath function and pass the “targetPath” we got from the developer (remember?) and the location of the base configuration path.

Now the generated files will contain the correct paths according to their location in the project’s tree. Pretty simple yet can save you a lot of headaches of “why doesn’t my new package resolve the path to my common Babel configuration?!”

Wrapping Up

Generators, and Plop in particular, are powerful tools that can make our development life much easier without investing too much into them. In this post you saw how you can make Plop’s templates even more robust.
As always, if you have other ideas on how to make files and code generators more efficient, or just have some questions about what was brought here, be sure to share with the rest of us in the comments section below.

Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .