Migrating an Angular project to an Nx workspace: Ng-Morph to the rescue!
Tags: ngmorph
Published: 2023-06-05 21:37:16

I am currently migrating multiple projects and shared libraries within an Nx workspace to take advantage of the power, tooling, and developer experience (DX) provided by Nx. I won't explain the process in this article, but during this transition, I had to perform numerous tedious and repetitive tasks. Fortunately, I discovered an amazing library called [**Ng-Morph**](https://tinkoff.github.io/ng-morph/getting-started) that allows us to write scripts for automating such repetitive tasks within our project tree.

In this article, I will explain three examples of scripts I wrote to speed up my work and make it less painful.

## Delete all unnecessary test files.
When generating a component, directive, or pipe using the Angular CLI, it creates a spec file with a single "should create" test. In my current workspace, many of these files were kept but are actually unnecessary and slow down the execution time of the test command. The goal is to delete all the files that only contain the "should create" test.

Using Ng-morph, we start by defining the file tree and the type of files we want to work with. It's mandatory to set our active project with the following line:

```ts
setActiveProject(createProject(new NgMorphTree(), '/', ['**/*.spec.ts']));
```

```ts
const sourceFiles = getSourceFiles(['apps/**/*.spec.ts', 'libs/**/*.spec.ts']);

sourceFiles.forEach(s => {
  const text = s.getFullText();
  if (text.match(new RegExp("(it\\('should be created'|it\\('should create)", 'g'))) {
    const secondTest = text.match(new RegExp("(it\\(')", 'g'));
    if (secondTest && secondTest.length === 1) {
      s.delete();
    }
  }
});
```
Next we load all spec files using `getSourceFiles`. Once done, we iterate through each file, check if it matches our criteria _(using a basic regex search on the file's text)_, and delete it if it does.

Finally, we save our changes by calling `saveActiveProject`.

As you can see, the script is simple and easy to write and read. It took me less than 15 minutes to write it, even without prior knowledge of the library. With practice, you can write this script in just 5 minutes. If you were to do this manually, it would take much longer and be less enjoyable.

> _One important thing to note is that there's no need to spend time writing the script with well-named variables or maintainable code, as the script is meant to be run once to accomplish the desired task._

Finally, to run the script, we use `ts-node`:

```bash
npx ts-node path/to/script
```

## Exporting all files to the public api file:
In Nx, we create libs/packages/modules _(name them as you like)_ to encapsulate code. To expose the code externally, we need to export it within a barrel file or public API file. In the current project I'm working on, each function/class is imported directly from its respective file, resulting in paths like this:

```ts
import { FooComponent } from 'src/path/to/foo.component';
```
However in Nx, if you want to use FooComponentin another library, we need to export it from the file that exports our library's API. Manually doing this can be tedious, as we would need to find and export the paths of all the files within the library.

With Ng-Morph, we can automate this task quickly. Let's take a look at the script:

```ts
const project = process.argv[2];

const folder = `libs/${project}`;

const fs = require('fs');
if (!fs.existsSync(folder)) {
  console.log("name of project doesn't exist");
  process.exit();
}
```
This time, I decided to make the script accept one argument, as it can be used for many other libraries.

```ts
setActiveProject(createProject(new NgMorphTree(), '/', ['**/*.ts']));
```
As mentionned before, we must setup the active project to register our tree.

```ts
const sourceFiles = getSourceFiles([`/${folder}/src/lib/**/*.ts`, `!/${folder}/src/lib/**/*.spec.ts`])
  .map(s => s.getFilePath().replace(`/${folder}/src`, '.').replace('.ts', ''))
  .map(n => `export * from '${n}';`);

const barrelFile = sourceFiles.reduce((acc, file) => `${acc}\n${file}`, '');

createSourceFile(`/${folder}/src/index.ts`, barrelFile, {
  overwrite: true
});

saveActiveProject();
```
All the logic is contained inside this few lines.

We load all TS files except spec files and we remove the `.ts` extenstion and add `export * from` statement to each file path.

Next, we concatenate all these strings into one and overwrite the barrel file of the library.

Finally, we don't forget to call `setActiveProject `to save our changes.

**Note:** This script is basic and can be optimized. It exports all files, including those not used outside the library. However, we are currently migrating multiple projects within Nx, and our priority is to make it work quickly. Other team members are working with the old setup, and the longer the branches diverge, the more challenging the merge will be.

In a following step, we can optimize the process by splitting the library and removing any unnecessary export files from the public API.

**Renaming file imports:**
This situation occurs frequently when files and functions are moved around. For example, we may have a large utils file and want to split it, moving some functions outside of that file and into a shared/utils library. As a result, the old imports must be removed and replaced with the new ones.

We can find imports like this:

```ts
import { addDate, removeDate } from '@test/shared'
import { addDate } from '@test/shared'
```
The second case can be easily handled with the search and replace feature in any IDE. However, the first case is more challenging because we need to remove `addDate `from the import array while keeping `removeDate `intact.

With Ng-morph, this task can be easily automated.

```ts
const importToReplace = 'addDate';
const newNamespace = '@test/shared/date-utils';

function importIfNotPresent(source: string) {
  addImports(source, [
    {
      namedImports: [importToReplace],
      moduleSpecifier: newNamespace
    }
  ]);
}

setActiveProject(createProject(new NgMorphTree(), '/', ['**/*.ts']));

const sourceFiles = getSourceFiles([`**/*.ts`]).map(s => s.getFilePath());

sourceFiles.forEach(s => {
  const imports = getImports(s, { namedImports: importToReplace }).filter(
    s => s.getModuleSpecifier().getText().replace(/'/g, '') !== newNamespace
  );

  imports.forEach(i => {
    const namedImports = i.getNamedImports();

    if (namedImports.length === 1) {
      i.remove();
      importIfNotPresent(s);
    } else {
      namedImports.forEach(n => {
        if (n.getName() === importToReplace) {
          n.remove();
          importIfNotPresent(s);
        }
      });
    }
  });
});

saveActiveProject();
```
The script is very easy to read and understand but let's break it:

First, we define the import we want to replace `importToReplace `and the new namespace `newNamespace `where the function will be located.

As usual, we start by calling `setActiveProject `to initiate our script. We load all TS file and iterate over them.

We search for occurrences `addDate `within the imports of each file and filter to ensure that the namespace hasn't already been updated.

Then, we iterate over the named imports. If there is only one import, we delete the entire import statement and replace it with the new one.

If there are multiple imports, we delete the desired function and add the new import statement to the file.

**Note:** As you can see, we don't need to work with complex AST function or perform complex searches. Ng-Morph allows us to access and modify our files in a simple and straightforward way.

Once you understand how to use Ng-Morph, working with your entire repository becomes easy and enjoyable.

> _Documentation of Ng-Morph can be found [here](https://tinkoff.github.io/ng-morph/getting-started)._

---

I hope you have a better understanding of this amazing library and have seen all the incredible things you can achieve with it. 🚀

You can find me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge).Don't hesitate to reach out to me if you have any questions.