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();
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',
});
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;
}
}
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'],
}));
}
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());
});
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)