@@ -3,6 +3,19 @@ import * as path from 'path';
3
3
import { SchematicContext , Tree } from '@angular-devkit/schematics' ;
4
4
import { WorkspaceSchema , WorkspaceProject , ProjectType } from '@schematics/angular/utility/workspace-models' ;
5
5
import { execSync } from 'child_process' ;
6
+ import {
7
+ Attribute ,
8
+ Comment ,
9
+ Element ,
10
+ Expansion ,
11
+ ExpansionCase ,
12
+ getHtmlTagDefinition ,
13
+ HtmlParser ,
14
+ Node ,
15
+ Text ,
16
+ Visitor
17
+ } from '@angular/compiler' ;
18
+ import { replaceMatch } from './tsUtils' ;
6
19
7
20
const configPaths = [ '/.angular.json' , '/angular.json' ] ;
8
21
@@ -111,3 +124,150 @@ export function tryUninstallPackage(context: SchematicContext, packageManager: s
111
124
. warn ( `Could not uninstall ${ pkg } , you may want to uninstall it manually.` , JSON . parse ( e ) ) ;
112
125
}
113
126
}
127
+
128
+ interface TagOffset {
129
+ start : number ;
130
+ end : number ;
131
+ }
132
+
133
+ export interface SourceOffset {
134
+ startTag : TagOffset ;
135
+ endTag : TagOffset ;
136
+ file : {
137
+ content : string ;
138
+ url : string ;
139
+ } ;
140
+ node ?: Element ;
141
+ }
142
+
143
+
144
+ export class FileChange {
145
+
146
+ constructor (
147
+ public position = 0 ,
148
+ public text = '' ,
149
+ public replaceText = '' ,
150
+ public type : 'insert' | 'replace' = 'insert'
151
+ ) { }
152
+
153
+ apply ( content : string ) {
154
+ if ( this . type === 'insert' ) {
155
+ return `${ content . substring ( 0 , this . position ) } ${ this . text } ${ content . substring ( this . position ) } ` ;
156
+ }
157
+ return replaceMatch ( content , this . replaceText , this . text , this . position ) ;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Parses an Angular template file/content and returns an array of the root nodes of the file.
163
+ *
164
+ * @param host
165
+ * @param filePath
166
+ * @param encoding
167
+ */
168
+ export function parseFile ( host : Tree , filePath : string , encoding = 'utf8' ) {
169
+ return new HtmlParser ( ) . parse ( host . read ( filePath ) . toString ( encoding ) , filePath ) . rootNodes ;
170
+ }
171
+
172
+ export function findElementNodes ( root : Node [ ] , tag : string | string [ ] ) : Node [ ] {
173
+ const tags = new Set ( Array . isArray ( tag ) ? tag : [ tag ] ) ;
174
+ return flatten ( Array . isArray ( root ) ? root : [ root ] )
175
+ . filter ( ( node : Element ) => tags . has ( node . name ) ) ;
176
+ }
177
+
178
+ export function hasAttribute ( root : Element , attribute : string | string [ ] ) {
179
+ const attrs = Array . isArray ( attribute ) ? attribute : [ attribute ] ;
180
+ return ! ! root . attrs . find ( a => attrs . includes ( a . name ) ) ;
181
+ }
182
+
183
+ export function getAttribute ( root : Element , attribute : string | string [ ] ) {
184
+ const attrs = Array . isArray ( attribute ) ? attribute : [ attribute ] ;
185
+ return root . attrs . filter ( a => attrs . includes ( a . name ) ) ;
186
+ }
187
+
188
+ export function getSourceOffset ( element : Element ) : SourceOffset {
189
+ const { startSourceSpan, endSourceSpan } = element ;
190
+ return {
191
+ startTag : { start : startSourceSpan . start . offset , end : startSourceSpan . end . offset } ,
192
+ endTag : { start : endSourceSpan . start . offset , end : endSourceSpan . end . offset } ,
193
+ file : {
194
+ content : startSourceSpan . start . file . content ,
195
+ url : startSourceSpan . start . file . url
196
+ } ,
197
+ node : element
198
+ } ;
199
+ }
200
+
201
+
202
+ function isElement ( node : Node | Element ) : node is Element {
203
+ return ( node as Element ) . children !== undefined ;
204
+ }
205
+
206
+ /**
207
+ * Given an array of `Node` objects, flattens the ast tree to a single array.
208
+ * De facto only `Element` type objects have children.
209
+ *
210
+ * @param list
211
+ */
212
+ export function flatten ( list : Node [ ] ) {
213
+ let node : Node ;
214
+ let r : Node [ ] = [ ] ;
215
+
216
+ for ( let i = 0 ; i < list . length ; i ++ ) {
217
+ node = list [ i ] ;
218
+ r . push ( node ) ;
219
+
220
+ if ( isElement ( node ) ) {
221
+ r = r . concat ( flatten ( node . children ) ) ;
222
+ }
223
+ }
224
+ return r ;
225
+ }
226
+
227
+ /**
228
+ * https://github.com/angular/angular/blob/master/packages/compiler/test/ml_parser/util/util.ts
229
+ *
230
+ * May be useful for validating the output of our own migrations,
231
+ */
232
+ class SerializerVisitor implements Visitor {
233
+
234
+ visitElement ( element : Element , context : any ) : any {
235
+ if ( getHtmlTagDefinition ( element . name ) . isVoid ) {
236
+ return `<${ element . name } ${ this . _visitAll ( element . attrs , ' ' ) } />` ;
237
+ }
238
+
239
+ return `<${ element . name } ${ this . _visitAll ( element . attrs , ' ' ) } >${ this . _visitAll ( element . children ) } </${ element . name } >` ;
240
+ }
241
+
242
+ visitAttribute ( attribute : Attribute , context : any ) : any {
243
+ return attribute . value === '' ? `${ attribute . name } ` : `${ attribute . name } ="${ attribute . value } "` ;
244
+ }
245
+
246
+ visitText ( text : Text , context : any ) : any {
247
+ return text . value ;
248
+ }
249
+
250
+ visitComment ( comment : Comment , context : any ) : any {
251
+ return `<!--${ comment . value } -->` ;
252
+ }
253
+
254
+ visitExpansion ( expansion : Expansion , context : any ) : any {
255
+ return `{${ expansion . switchValue } , ${ expansion . type } ,${ this . _visitAll ( expansion . cases ) } }` ;
256
+ }
257
+
258
+ visitExpansionCase ( expansionCase : ExpansionCase , context : any ) : any {
259
+ return ` ${ expansionCase . value } {${ this . _visitAll ( expansionCase . expression ) } }` ;
260
+ }
261
+
262
+ private _visitAll ( nodes : Node [ ] , join : string = '' ) : string {
263
+ if ( nodes . length === 0 ) {
264
+ return '' ;
265
+ }
266
+ return join + nodes . map ( a => a . visit ( this , null ) ) . join ( join ) ;
267
+ }
268
+ }
269
+
270
+
271
+ export function serializeNodes ( nodes : Node [ ] ) : string [ ] {
272
+ return nodes . map ( node => node . visit ( new SerializerVisitor ( ) , null ) ) ;
273
+ }
0 commit comments