This is the app.module.ts file I want to read (Written In TypeScript).
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
Then, basically I want to programatically add another component like this:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroesComponent } from './heroes/heroes.component';
import { ShowsComponent } from './shows/shows.component';//** NEW LINE
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent },
{ path: 'shows', component: ShowsComponent }//** New Line
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
So, the program would receive the original file as input and then when finish the original file would be modified with the new code. The "component" to be added can be received as a parameter.
How would you go about solving this?
PD: I want to do it so I detect the symbols properly. I mean, the code has to work as long as the syntax of the target TS file is valid.
PD2: I've been checking the Compiler Api https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API https://www.npmjs.com/package/ts-node
Thanks!
I would go with typescript compiler API.
Let's say we add import at the end of all other imports and also add route at the end of all other routes.
Here's a simplified version. You can improve it if:
you need to check if there is already added route.
or for instance if there is added import path then we do not need to add new import but rather add clause to exising.
or something else depending on your demands.
interface Replacement {
atPosition: number;
toInsert: string;
}
function replace(content: string) {
const sourceFile = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true);
const replacements: Replacement[] = [
addRoute({ path: 'shows', component: 'ShowsComponent' }, sourceFile),
addImport({ name: 'ShowsComponent', path: './shows/shows.component'}, sourceFile)
]
for (const replacement of replacements) {
content = content.substring(0, replacement.atPosition) + replacement.toInsert + content.substring(replacement.atPosition);
}
return content;
}
function addRoute(route: { path: string, component: string }, sourceFile: ts.SourceFile): Replacement {
const routesDefinition = getRoutesArrayNode(sourceFile);
const routes = findNodes(routesDefinition, ts.SyntaxKind.ObjectLiteralExpression);
const toInsert = `,\n { path: '${route.path}', component: ${route.component} }`;
return insertToTheEnd(routes, toInsert);
}
function addImport(toImport: { name: string, path: string }, sourceFile: ts.SourceFile): Replacement {
const allImports = findNodes(sourceFile, ts.SyntaxKind.ImportDeclaration);
const toInsert = `\nimport { ${toImport.name} } from '${toImport.path}';`;
return insertToTheEnd(allImports, toInsert);;
}
function insertToTheEnd(nodes: any[], toInsert: string): Replacement {
const lastItem = nodes.sort((first: ts.Node, second: ts.Node): number => first.getStart() - second.getStart()).pop();
const atPosition: number = lastItem.getEnd();
return { atPosition, toInsert };
}
function getRoutesArrayNode(sourceFile: ts.SourceFile): ts.Node {
let result: ts.Node | null = null;
ts.forEachChild(sourceFile, (node) => {
if (node.kind === ts.SyntaxKind.VariableStatement) {
const variableStatement = <ts.VariableStatement>node;
for (const variableDeclaration of variableStatement.declarationList.declarations) {
if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier && variableDeclaration.initializer) {
const initializerNode = variableDeclaration.initializer;
if (initializerNode.kind === ts.SyntaxKind.ArrayLiteralExpression) {
if (isRoutesArray(variableDeclaration)) {
result = initializerNode;
}
}
}
}
}
});
return result;
}
function isRoutesArray(node: ts.Node): boolean {
let result = false;
ts.forEachChild(node, child => {
if (child.kind === ts.SyntaxKind.TypeReference) {
const typeReferenceNode = <ts.TypeReferenceNode>child;
const typeNameNode = typeReferenceNode.typeName;
if (typeNameNode.text === 'Routes') {
result = true;
}
}
});
return result;
}
function findNodes(node: ts.Node, kind: ts.SyntaxKind): any[] {
const arr: any[] = [];
if (node.kind === kind) {
arr.push(node);
}
for (const child of node.getChildren()) {
findNodes(child, kind).forEach(node => {
arr.push(node);
});
}
return arr;
}
These links might be also helpful for you:
https://github.com/angular/devkit/blob/master/packages/schematics/angular/utility/ast-utils.ts
https://github.com/angular/angular/blob/2c2b62f45f29e7658028d85be5a26db812c0525d/packages/compiler-cli/src/metadata/evaluator.ts#L253
P.S. If you're on node.js then I suppose you know how work with fs.readFile and fs.writeFile :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With