Developing template scaffolding CLI util with Typescript and Clean Architecture
nikita
Posted on February 1, 2021
Intro
Everyone involved in writing code sometimes deals with copy and pasting at some point. It may be a single file, a small folder of related files with specific structure, or even a project boilerplate.
As a React developer I used to do this ALOT. When it comes to creating a new component I usually copy the folder of the component I've already created.
Lets assume I want to create a Title component. I start by copying the nearest Button component:
find and replace all occurrences of Button in source code
get rid of redundant props, imports, etc
š¤¦
So after a small research I decided to create a simple CLI tool that would save me some time by providing a short npx command. Something like
npx mycooltool rfc ./components
where rfc is the name of the template (React functional component) and ./components is the path to put it in.
Rest of the article will guide you through the development process of the above CLI utility but looking ahead, if you want to jump straight to code, this is what I came up with:
A CLI utility library for scaffolding code templates and boilerplates.
bystro
A CLI utility library for scaffolding code templates and boilerplates
Sometimes you can find yourself copypasting a whole folder of files which represents some component (f.e. React component) and then renaming filenames, variables, etc. to satisfy your needs. Bystro helps you to automate this process.
Install
$ npm install -D bystro
Usage
$ bystro <template_name><path>
Note: You can alternatively run npx bystro <template_name> <path> without install step.
Arguments
<template_name> - Name of the template you want to scaffold. <path>- Path to scaffold template in.
Before writing any code I found it reasonable to put together some small description of the algorithm that I expect from my CLI tool to implement:
Obtain user input (<template_name> and <path_to_clone_into>) from the command line.
Get template data by <template_name> by checking custom templates folder created by user and if not found fetch it from predefined templates folder inside of the package itself.
If template was found prompt user to fill the variables required in the template.
Interpolate template filenames and contents according to users input.
Write result template files into <path_to_clone_into>
Architecture
For now it is totally fine to store shared templates inside of package source code but the bigger it gets the slower npx will execute it.
Later we could implement templates search using github api or something. That is why we need our code to be loosely coupled to easily switch between template repository implementations.
Clean architecture to the rescue. I won't go in much details explaining it here, there are a lot of great official resources to read. Just take a look at the diagram which describes the main idea:
CA states that source code dependencies can only point inwards, the inner circles must be unaware of the outer ones and that they should communicate by passing simple Data Transfer objects. Let's take all the about literally and start writing some code.
Business rules
If we take a closer look at the algorithm we've defined at planning phase it seems like it's a perfect example of a use case. So let's implement right away:
importTemplatefrom"entities/Template"exportdefaultclassScaffoldTemplateIntoPath{constructor(privatetemplatesRepository:ITemplatesRepository,privatefs:IFileSystemAdapter,privateio:IInputOutputAdapter,){}publicexec=async (path:string,name:string)=>{if (!name)thrownewError("Template name is required");// 2. Get template data by <template_name>constdto=this.templatesRepository.getTemplateByName(name);if (!dto)thrownewError(`Template "${name}" was not found`);// 3. If template was found prompt user to fill the variableconsttemplate=Template.make(dto).setPath(path);constvariables=awaitthis.io.promptInput(template.getRequiredVariables());// 4. Interpolate template filenames and contentsconstfiles=template.interpolateFiles(values);// 5. Write modified filesreturnthis.fs.createFiles(files);};}
As you can see Template is the only direct dependency while ITemplatesRepository, IFileSystemAdapter and IInputOutputAdapter are injected (inverted) which means they don't break the dependency rule. This fact gives us a lot of flexibility since their possible changes won't affect ScaffoldTemplateIntoPath usecase and we can easily mock them out in test environment.
With this in place we already have our basic business rules ready to be used within basically any javascript environment. It may be a CLI tool, REST API, frontend app, browser extension etc.
Feel free to check out the source code for the outer layers implementations:
Running npm run release or yarn release should check our files, run tests, bump version, and finally publish our project to npm registry.
ššš
Outro
I'm pretty excited to bring Clean Architecture into my project, but I want you not to take everything I say for granted. I'm learning as well so I might be wrong in understanding some CA concepts and would love to hear some feedback.
BTW this is my first article ever as well as the first open source project and I would be happy to hear from the community. Any feedback, pull requests, open issues would be great. Let me know what you think.