Developing template scaffolding CLI util with Typescript and Clean Architecture
Tags: showdev, webdev, codequality, typescript
Published: 2021-02-01 01:09:03

## 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:

```
|____Button
| |____Button.module.scss
| |____Button.stories.mdx
| |____Button.tsx
| |____index.ts
| |____README.md
```

Then I need to:
- rename folder and filenames
- find and replace all occurrences of `Button` in source code
- get rid of redundant `props`, `import`s, etc
- 🤦

<hr>

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:

{% github streletss/bystro %}

## Planning

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:

1. Obtain user input (`<template_name>` and `<path_to_clone_into>`) from the command line.

2. 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.

3. If template was found prompt user to fill the variables required in the template.

4. Interpolate template filenames and contents according to users input.

5. 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](https://docs.github.com/en/rest/reference/repos#get-repository-content) 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](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). Just take a look at the diagram which describes the main idea:

![CleanArchitecture](https://dev-to-uploads.s3.amazonaws.com/i/uio7zb77u8e9o2dp7x8w.jpg)
 

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:

```typescript
import Template from "entities/Template"

export default class ScaffoldTemplateIntoPath {
  constructor(
    private templatesRepository: ITemplatesRepository,
    private fs: IFileSystemAdapter,
    private io: IInputOutputAdapter,
  ) {}

  public exec = async (path: string, name: string) => {
    if (!name) throw new Error("Template name is required");

    // 2. Get template data by <template_name>
    const dto = this.templatesRepository.getTemplateByName(name);
    if (!dto) throw new Error(`Template "${name}" was not found`);

    // 3. If template was found prompt user to fill the variable
    const template = Template.make(dto).setPath(path);
    const variables = await this.io.promptInput(template.getRequiredVariables());

    // 4. Interpolate template filenames and contents
    const files = template.interpolateFiles(values);

    // 5. Write modified files
    return this.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.

Let's now implement the `Template` entity:

```typescript
class Template implements ITemplateInstance {
  constructor(private dto: ITemplate) {
    this.dto = { ...dto };
  }

  public getPath = () => {
    return this.dto.path;
  };

  public getRequiredVariables = () => {
    return this.dto.config.variables;
  };

  public getFiles = () => {
    return this.dto.files;
  };

  public interpolateFiles = (variables: Record<string, string>) => {
    return this.dto.files.map((file) => {
      // private methods
      file = this.interpolateFile(file, variables); 
      file.path = this.toFullPath(file.path);
      return file;
    });
  };

  public setPath = (newPath: string) => {
    this.dto.path = newPath;
    return this;
  };

  public toDTO = () => {
    return this.dto;
  };

  // ...
}
```

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:

{% github streletss/bystro no-readme %}


## Publish to `npm`

To make the project globally available as an executable `npm` package we need to do the following in `package.json`:

```json
{
  "name": "bystro",
  "version": "0.1.0",
  "bin": "./dist/index.js", // compiled code
  // ...
}
```

By setting `bin` field `npm` should treat our project as an executable by putting a symlink named `bystro` into `./node_modules/.bin` on install.

Lastly after compiling our `.ts` code we will use [np](https://github.com/sindresorhus/np) package to help us to efficiently publish our package.

```json
"scripts": {
  "build": "tsc -p .",
  "release": "yarn build && np", 
  // ...
}
```

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.

Thanks for reading 🙏