DEV Community

Cover image for ng-morph is the best way to do big changes in your Angular project
Roman Sedov
Roman Sedov

Posted on

ng-morph is the best way to do big changes in your Angular project

I have been working on big Angular projects for years and every project follows the same path: it starts with brand new tools and approaches that lay its development style for the future. Approaches and tools change with time and the project has tech debt around migrating to new package versions, replacing a deprecated approach with the new one, etc.

Sure, not every team can develop and support all the business needs and find the time to sync their code base with pioneering development methods. But what if I show you a tool with which you can transform the whole project just with several lines of codes?

What is ng-morph

ng-morph is a large set of tools for both global code base updates in your project and speeding up your work on Angular schematics. It has ts-morph under the hood and allows you to manipulate with TypeScript AST safely.

You do not need to know how TypeScript works under the hood to transform your project. You can use simple ready functions for manipulating TS and NG entities. Moreover ng-morph adds several benefits in the process:

  • You make changes in virtual file structure. So, you can check the new structure first, fix all the bugs and inconsistencies before saving changes in the real project structure. It is much faster than migrating a real project and reverting changes after checking. \

  • You can write unit tests for your functions and scripts before use it in real projects or share with a lot of colleges

It is easy to write migrations with ng-morph. You just need to invest time in your team once, and then this tool will save you several hours every week.

ng-morph has 1.6kk downloads on NPM in the last year. It is open source and fully free.

How to use it in your project

Well, let’s take a look at two common cases for ng-morph.

The first one is for library developers. If you want to publish breaking changes updates for your library, it is much better to add an “ng update” schematic that will make migrations of your users automatic and save a huge amount of time in total. For example, Taiga UI has used ng-morph to add such a migration for a major 3.0 release with many breaking changes. In such a big library even if you ask developers to do 1 minute manual migration in your project, it’ll take hundreds of working hours in total. You can find guidelines on how to use it in schematics in our docs.

The second case is writing migrations for your own project and this is a case that I want to show you in this article. You can write a simple script in any place of your project and run it with something like ts-node immediately.

import {
  setActiveProject,
  createProject,
  getImports,
  NgMorphTree,
} from 'ng-morph';

/**
 * set all ng-morph functions to work with the all TS and JSON files
 * of the current project
 * */
setActiveProject(
  createProject(new NgMorphTree(), '/', ['**/*.ts', '**/*.json'])
);

/**
 * This simple migration gets all imports from the project TS files and
 * replaces 'old' substring with 'new'
 * */
const imports = getImports('some/path/**.ts', {
    moduleSpecifier: '@morph-old*',
});

editImports(imports, importEntity => ({
    moduleSpecifier: importEntity.moduleSpecifier.replace('old', 'new')
}));

/**
 * All changes are made in a virtual project.
 * You can save them when it is time
 * */
saveActiveProject();
Enter fullscreen mode Exit fullscreen mode

Before reading about some real migrations, you can check our Stackblitz playground, run project migrations there and see how it works in real life.

Project migrations

Let’s take a look at an example on how easy it can be with ng-morph to make global changes in your project.

constructor -> inject

All right, Angular introduced using inject function in components and now we can replace type inaccurate constructor injection with it. Let’s write a ng-morph script in 30 minutes that migrates all the components in our project to the new approach.

const components = getClasses('**/*.ts', {
 name: '*Component',
});
Enter fullscreen mode Exit fullscreen mode

We can get all components in several ways. The totally right way is to find all the classes with decorator Component that are imported from the “@angular/core” package. But my idea is that usually we do not need such an overengineering approach writing simple migrations scripts inside our own project if we can get the same result faster.

components is an array of AST entities. We can iterate it using .forEach.

First of all, let’s skip the classes that do not inject anything. We can use the getConstructors function with our class to get its constructor. After it we can process the result with getParams function that will return us each injected param.

components.forEach((component) => {
   const constructorParams = getParams(getConstructors(component));

   if (constructorParams.length === 0) {
     return;
   }
}
Enter fullscreen mode Exit fullscreen mode

We should also add an inject function to our imports from “@angular/core”. This is how we can get all the imports and add a new one.

function fixInjectImport(file: string) {
 const angularCoreImports = getImports(file, {
   moduleSpecifier: '@angular/core',
 });

 editImports(angularCoreImports, (entity) => ({
   namedImports: [...entity.namedImports, 'inject'],
 }));
}
Enter fullscreen mode Exit fullscreen mode

All we need to do is add new properties to the class and remove them from the constructor. This is how it looks in code:

components.forEach((component) => {
 const constructorParams = getParams(getConstructors(component));

 if (constructorParams.length === 0) {
   return;
 }

 fixInjectImport(component.getSourceFile().getFilePath());

 addProperties(
   component,
   constructorParams.map((param) => ({
     name: param.getName(),
     type: param.getTypeNode().getText(),
     isReadonly: param.isReadonly(),
     scope: param.getScope(),
     initializer: `inject(${
       param.getDecorator('Inject')?.getArguments()[0].getText() ??
       param.getTypeNode().getText()
     })`,
   }))
 );

 constructorParams.forEach((param) => {
   param.remove();
 });

 console.log(component.getSourceFile().getText());
});
Enter fullscreen mode Exit fullscreen mode

I think there are no questions around a constructor params removing part. Let’s take a look at the addProperties function. This function takes a class (or classes) and a properties array to add to the class. We want to keep property with the same name, type, readonly status and scope (private/public/protected) as it was in param. And we need to initialize it with the default value that will be inject function with a token from Inject decorator if there is one, or from the type as a fallback. Actually, that is it, we can start the script and see how it migrates our project.

A task for you to try ng-morph

You can play with this example on Stackblitz and see how it works now.

You can also modify the script to handle @Optional and @Self DI decorators as well. It should not take much time even with no docs and if you've never used ng-morph before.

To be continued…

ng-morph can become your faithful friend when it comes to global changes in your project or routine work. So, don’t forget to give a star on Github and try to use it once you need it.

If it does not have something you need for smooth and easy migration, feel free to create an issue to let us help. By the way, you can also contribute it yourself because it is a great opportunity to learn more about your project structure from a compiler perspective. We have a CONTRIBUTING.md file with all the guidelines you need to start.

This is the first of three articles about ng-morph powers, so stay tuned here or on Twitter: @marsibarsi

If you want to get an expert help in big migrations of your project or make your developer experience better and faster, visit ng.consulting page

Top comments (0)