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