53
loading...
This website collects cookies to deliver better user experience
ng add
, ng update
and ng generate
use schematics to add, update and configure libraries and generate code for applications. In runtime, you get access to a virtual file system and can mutate source code as you need. "But for code mutation, I have to work with AST. It is so hard." — say you. And you are right!Rule
. Let's look at this type:type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void | Rule> | void;
Rule
can be synchronous or asynchronous. Like a bonus, Rule
can return Observable
.Tree
. Tree
is an abstraction for working with the virtual file system. Any changes in the virtual file system apply to the real file system.ng update
command. We even contributed to RenovateBot to run migrations automatically when the dependencies are updated.AppModule
. After development, we realized that we had spent much more time than expected.export function addSymbolToNgModuleMetadata(
source: ts.SourceFile,
ngModulePath: string,
metadataField: string,
symbolName: string,
importPath: string | null = null,
): Change[] {
const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
let node: any = nodes[0]; // tslint:disable-line:no-any
// Find the decorator declaration.
if (!node) {
return [];
}
// Get all the children property assignment of object literals.
const matchingProperties = getMetadataField(
node as ts.ObjectLiteralExpression,
metadataField,
);
// Get the last node of the array literal.
if (!matchingProperties) {
return [];
}
if (matchingProperties.length == 0) {
// We haven't found the field in the metadata declaration. Insert a new field.
const expr = node as ts.ObjectLiteralExpression;
let position: number;
let toInsert: string;
if (expr.properties.length == 0) {
position = expr.getEnd() - 1;
toInsert = ` ${metadataField}: [${symbolName}]\\n`;
} else {
node = expr.properties[expr.properties.length - 1];
position = node.getEnd();
// Get the indentation of the last element, if any.
const text = node.getFullText(source);
const matches = text.match(/^\\r?\\n\\s*/);
if (matches && matches.length > 0) {
toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
} else {
toInsert = `, ${metadataField}: [${symbolName}]`;
}
}
if (importPath !== null) {
return [
new InsertChange(ngModulePath, position, toInsert),
insertImport(source, ngModulePath, symbolName.replace(/\\..*$/, ''), importPath),
];
} else {
return [new InsertChange(ngModulePath, position, toInsert)];
}
}
const assignment = matchingProperties[0] as ts.PropertyAssignment;
// If it's not an array, nothing we can do really.
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
return [];
}
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
if (arrLiteral.elements.length == 0) {
// Forward the property.
node = arrLiteral;
} else {
node = arrLiteral.elements;
}
if (!node) {
// tslint:disable-next-line: no-console
console.error('No app module found. Please add your new class to your component.');
return [];
}
if (Array.isArray(node)) {
const nodeArray = node as {} as Array<ts.Node>;
const symbolsArray = nodeArray.map(node => node.getText());
if (symbolsArray.includes(symbolName)) {
return [];
}
node = node[node.length - 1];
}
let toInsert: string;
let position = node.getEnd();
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
// We haven't found the field in the metadata declaration. Insert a new
// field.
const expr = node as ts.ObjectLiteralExpression;
if (expr.properties.length == 0) {
position = expr.getEnd() - 1;
toInsert = ` ${symbolName}\\n`;
} else {
// Get the indentation of the last element, if any.
const text = node.getFullText(source);
if (text.match(/^\\r?\\r?\\n/)) {
toInsert = `,${text.match(/^\\r?\\n\\s*/)[0]}${symbolName}`;
} else {
toInsert = `, ${symbolName}`;
}
}
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
// We found the field but it's empty. Insert it just before the `]`.
position--;
toInsert = `${symbolName}`;
} else {
// Get the indentation of the last element, if any.
const text = node.getFullText(source);
if (text.match(/^\\r?\\n/)) {
toInsert = `,${text.match(/^\\r?\\n(\\r?)\\s*/)[0]}${symbolName}`;
} else {
toInsert = `, ${symbolName}`;
}
}
if (importPath !== null) {
return [
new InsertChange(ngModulePath, position, toInsert),
insertImport(source, ngModulePath, symbolName.replace(/\\..*$/, ''), importPath),
];
}
return [new InsertChange(ngModulePath, position, toInsert)];
}
It has ts-morph under the hood and allows you to manipulate with safe TypeScript AST.
ng-morph
is a set of utilities that will allow you to write schematics much easier and faster. Let's look at a few examples of using it.const rule: Rule = (tree: Tree, context: SchematicContext): void => {
setActiveProject(createProject(tree));
const appModule = getMainModule('src/main.ts');
addImportToNgModule(appModule, 'SomeModule');
addImports(appModule.getFilePath(), {moduleSpecifier: '@some/package', namedExports: ['SomeModule']})
saveActiveProject();
}
ng-morph
project and set it active. It is important because all of the functions work in the context of the active project. Project is a class with access to a file system, the TS compiler, etc.ng-morph
, you probably won't have to write something like this.ng-morph
power!setActiveProject(createProject(new NgMorphTree('/')));
const enums = getEnums('/**/*.ts');
editEnums(enums, ({name}) => ({name: name.toUpperCase()}))
Tree
is created manually with NgMorphTree
.ng-morph
can work outside of schematics! And yes, we use ng-morph
in non-Angular projects!createImports('/src/some.ts', [
{
namedImports: ['CoreModule'],
moduleSpecifier: '@org/core',
isTypeOnly: true,
}
]);
const imports = getImports('src/**/*.ts', {
moduleSpecifier: '@org/*',
});
editImports(imports, ({moduleSpecifier}) => ({
moduleSpecifier: moduleSpecifier.replace('@org', '@new-org')
})
removeImports(imports)
get*
, edit*
, add*
, remove*
). For example getClass
, removeConstrucor
, addDecorator
. We started to develop utility functions for working with Angular-specific cases:getBootstrapFn
is a function that returns CallExpression
getMainModule
is a function that returns the main module declaration.addDeclarationToNgModule
, addProviderToDirective
, etc.ng-morph
can work with json
. For example, you can add dependencies in package.json
:addPackageJsonDependency(tree, {
name: '@package/name',
version: '~2.0.0',
type: NodeDependencyType.Dev
});
ts-morph API
and fall even lower into the typescript API.