Skip to content

Commit cf2d9d6

Browse files
committed
feat: add utilities for typescript ast
'newroute-utility.ts' provides typescript utility functions to be used in the new generate router command
1 parent 327f649 commit cf2d9d6

File tree

4 files changed

+556
-1
lines changed

4 files changed

+556
-1
lines changed

addon/ng2/utilities/dynamic-path-parser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ module.exports = function dynamicPathParser(project, entityName) {
5555
parsedPath.appRoot = appRoot
5656

5757
return parsedPath;
58-
};
58+
};
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import * as ts from 'typescript';
2+
import * as fs from 'fs';
3+
import * as edit from './change';
4+
5+
export function removeRouteFromParent(){
6+
7+
}
8+
9+
export function findParentRouteFile(){
10+
11+
}
12+
13+
export function addRoutesToParent(){
14+
15+
}
16+
17+
/**
18+
* Adds provideRouter configuration to the main file (import and bootstrap) if
19+
* main file hasn't been already configured, else it has no effect.
20+
*
21+
* @param (mainFile) path to main.ts in ng project
22+
*/
23+
export function configureMain(mainFile: string): Promise<void>{
24+
return insertImport(mainFile, 'provideRouter', '@angular/router')
25+
.then(() => {
26+
let rootNode = ts.createSourceFile(mainFile, fs.readFileSync(mainFile).toString(),
27+
ts.ScriptTarget.ES6, true);
28+
let paths = '[]';
29+
let expressions = findNodes(rootNode, ts.SyntaxKind.ExpressionStatement);
30+
let isBootstraped = false;
31+
let bootstrapNode = expressions.filter(node => {
32+
let st = findNodes(node, ts.SyntaxKind.Identifier).filter(n => {
33+
if (n.text === 'provideRouter') { // checks whether to insert provideRouter
34+
isBootstraped = true;
35+
}
36+
return n.text.toLowerCase() === 'bootstrap';
37+
});
38+
return st.length === 1;
39+
}).pop();
40+
41+
if (isBootstraped) {
42+
return Promise.resolve();
43+
}
44+
45+
// if bracket exitst already, add configuration template,
46+
// otherwise, insert into bootstrap parens
47+
let bootstrapProviders = findNodes(bootstrapNode, ts.SyntaxKind.ArrayLiteralExpression);
48+
49+
var fallBackPos: number, configurePathsTemplate: string, separator: string;
50+
51+
if (bootstrapProviders.length > 0) {
52+
fallBackPos = findNodes(bootstrapNode, ts.SyntaxKind.CloseBracketToken).pop().pos;
53+
// count content of provider array. Child at position 1 is the
54+
// node of syntaxLists within the '[' ']' literals
55+
let providerCount: number = bootstrapProviders.pop().getChildAt(1).getChildCount();
56+
configurePathsTemplate = `provideRouter(${paths})`;
57+
separator = providerCount == 0 ? '' : ', ';
58+
} else {
59+
fallBackPos = findNodes(bootstrapNode, ts.SyntaxKind.CloseParenToken).pop().pos;
60+
configurePathsTemplate = `, [provideRouter(${paths})]`;
61+
separator = '';
62+
}
63+
return insertAfterLastOccurence(bootstrapProviders, separator, configurePathsTemplate,
64+
mainFile, fallBackPos, ts.SyntaxKind.SyntaxList);
65+
});
66+
}
67+
68+
/**
69+
* Add Import `import { symbolName } from fileName` if the import doesn't exit
70+
* already. Assumes fileToEdit can be resolved and accessed.
71+
* @param fileToEdit (file we want to add import to)
72+
* @param symbolName (item to import)
73+
* @param fileName (path to the file)
74+
*/
75+
76+
export function insertImport(fileToEdit: string, symbolName: string, fileName: string): Promise<void> {
77+
let rootNode = ts.createSourceFile(fileToEdit, fs.readFileSync(fileToEdit).toString(),
78+
ts.ScriptTarget.ES6, true);
79+
let allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
80+
81+
// get nodes that map to import statements from the file fileName
82+
let relevantImports = allImports.filter(node => {
83+
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
84+
let importFiles = node.getChildren().filter(child => child.kind === ts.SyntaxKind.StringLiteral)
85+
.map(n => (<ts.StringLiteralTypeNode>n).text);
86+
return importFiles.filter(file => file === fileName).length === 1;
87+
});
88+
89+
if (relevantImports.length > 0) {
90+
91+
var importsAsterisk: boolean = false;
92+
// imports from import file
93+
let imports: ts.Node[] = [];
94+
relevantImports.forEach(n => {
95+
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
96+
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
97+
importsAsterisk = true;
98+
}
99+
});
100+
101+
// if imports * from fileName, don't add symbolName
102+
if (importsAsterisk) {
103+
return Promise.resolve();
104+
}
105+
106+
let importTextNodes = imports.filter(n => (<ts.Identifier>n).text === symbolName);
107+
108+
// insert import if it's not there
109+
if (importTextNodes.length === 0) {
110+
let fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
111+
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
112+
return insertAfterLastOccurence(imports, ', ', symbolName, fileToEdit, fallbackPos);
113+
}
114+
return Promise.resolve();
115+
}
116+
// no such import declaration exists
117+
let useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(n => n.text === 'use strict');
118+
let fallbackPos: number = 0;
119+
if(useStrict.length > 0){
120+
fallbackPos = useStrict[0].end;
121+
}
122+
return insertAfterLastOccurence(allImports, ';\n', 'import { ' + symbolName + ' } from \'' + fileName + '\'',
123+
fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral)
124+
};
125+
126+
/**
127+
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
128+
* @param node
129+
* @param kind (a valid index of ts.SyntaxKind enum, eg ts.SyntaxKind.ImportDeclaration)
130+
* @return all nodes of kind kind, or [] if none is found
131+
*/
132+
export function findNodes (node: ts.Node, kind: number, arr: ts.Node[] = []): ts.Node[]{
133+
if (node) {
134+
if(node.kind === kind){
135+
arr.push(node);
136+
}
137+
node.getChildren().forEach(child => findNodes(child, kind, arr));
138+
}
139+
return arr;
140+
}
141+
142+
/**
143+
* @param nodes (nodes to sort)
144+
* @return (nodes sorted by their position from the source file
145+
* or [] if nodes is empty)
146+
*/
147+
export function sortNodesByPosition(nodes: ts.Node[]): ts.Node[]{
148+
if (nodes) {
149+
return nodes.sort((first, second) => {return first.pos - second.pos});
150+
}
151+
return [];
152+
}
153+
154+
/**
155+
*
156+
* Insert toInsert after the last occurence of ts.SyntaxKind[nodes[i].kind]
157+
* or after the last of occurence of syntaxKind if the last occurence is a sub child
158+
* of ts.SyntaxKind[nodes[i].kind]
159+
* @param nodes (insert after the last occurence of nodes)
160+
* @param toInsert (string to insert)
161+
* @param separator (separator between existing text that comes before
162+
* the new text and toInsert)
163+
* @param file (file to write the changes to)
164+
* @param fallbackPos (position to insert if toInsert happens to be the first occurence)
165+
* @param syntaxKind (the ts.SyntaxKind of the last subchild of the last
166+
* occurence of nodes, after which we want to insert)
167+
* @throw Error if toInsert is first occurence but fall back is not set
168+
*/
169+
export function insertAfterLastOccurence(nodes: ts.Node[], separator: string, toInsert:string,
170+
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Promise<void> {
171+
var lastItem = sortNodesByPosition(nodes).pop();
172+
173+
if (syntaxKind) {
174+
lastItem = sortNodesByPosition(findNodes(lastItem, syntaxKind)).pop(;
175+
}
176+
if (!lastItem && fallbackPos === undefined) {
177+
return Promise.reject(new Error(`tried to insert ${toInsert} as first occurence with no fallback position`));
178+
}
179+
let lastItemPosition: number = lastItem? lastItem.end : fallbackPos;
180+
181+
let editFile = new edit.InsertChange(file, lastItemPosition, separator + toInsert);
182+
return editFile.apply();
183+
}
184+

0 commit comments

Comments
 (0)