| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							|  |  |  |  * Copyright Google Inc. All Rights Reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import * as ts from 'typescript'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface Symbol { name: string; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export class SymbolExtractor { | 
					
						
							|  |  |  |   public actual: Symbol[]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static symbolSort(a: Symbol, b: Symbol): number { | 
					
						
							|  |  |  |     return a.name == b.name ? 0 : a.name < b.name ? -1 : 1; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static parse(path: string, contents: string): Symbol[] { | 
					
						
							|  |  |  |     const symbols: Symbol[] = []; | 
					
						
							|  |  |  |     const source: ts.SourceFile = ts.createSourceFile(path, contents, ts.ScriptTarget.Latest, true); | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |     let fnRecurseDepth = 0; | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |     function visitor(child: ts.Node) { | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |       // Left for easier debugging.
 | 
					
						
							|  |  |  |       // console.log('>>>', ts.SyntaxKind[child.kind]);
 | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |       switch (child.kind) { | 
					
						
							|  |  |  |         case ts.SyntaxKind.FunctionExpression: | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           fnRecurseDepth++; | 
					
						
							|  |  |  |           if (fnRecurseDepth <= 1) { | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |             ts.forEachChild(child, visitor); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           fnRecurseDepth--; | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |           break; | 
					
						
							|  |  |  |         case ts.SyntaxKind.SourceFile: | 
					
						
							|  |  |  |         case ts.SyntaxKind.VariableStatement: | 
					
						
							|  |  |  |         case ts.SyntaxKind.VariableDeclarationList: | 
					
						
							|  |  |  |         case ts.SyntaxKind.ExpressionStatement: | 
					
						
							|  |  |  |         case ts.SyntaxKind.CallExpression: | 
					
						
							|  |  |  |         case ts.SyntaxKind.ParenthesizedExpression: | 
					
						
							|  |  |  |         case ts.SyntaxKind.Block: | 
					
						
							|  |  |  |         case ts.SyntaxKind.PrefixUnaryExpression: | 
					
						
							|  |  |  |           ts.forEachChild(child, visitor); | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         case ts.SyntaxKind.VariableDeclaration: | 
					
						
							|  |  |  |           const varDecl = child as ts.VariableDeclaration; | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           if (varDecl.initializer && fnRecurseDepth !== 0) { | 
					
						
							| 
									
										
										
										
											2019-01-12 00:59:48 -08:00
										 |  |  |             symbols.push({name: stripSuffix(varDecl.name.getText())}); | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-06-20 16:00:18 -07:00
										 |  |  |           if (fnRecurseDepth == 0 && isRollupExportSymbol(varDecl)) { | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |             ts.forEachChild(child, visitor); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |           break; | 
					
						
							|  |  |  |         case ts.SyntaxKind.FunctionDeclaration: | 
					
						
							|  |  |  |           const funcDecl = child as ts.FunctionDeclaration; | 
					
						
							| 
									
										
										
										
											2019-01-12 00:59:48 -08:00
										 |  |  |           funcDecl.name && symbols.push({name: stripSuffix(funcDecl.name.getText())}); | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |           break; | 
					
						
							|  |  |  |         default: | 
					
						
							|  |  |  |           // Left for easier debugging.
 | 
					
						
							|  |  |  |           // console.log('###', ts.SyntaxKind[child.kind], child.getText());
 | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     visitor(source); | 
					
						
							|  |  |  |     symbols.sort(SymbolExtractor.symbolSort); | 
					
						
							|  |  |  |     return symbols; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  |   static diff(actual: Symbol[], expected: string|((Symbol | string)[])): {[name: string]: number} { | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |     if (typeof expected == 'string') { | 
					
						
							|  |  |  |       expected = JSON.parse(expected); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  |     const diff: {[name: string]: number} = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // All symbols in the golden file start out with a count corresponding to the number of symbols
 | 
					
						
							|  |  |  |     // with that name. Once they are matched with symbols in the actual output, the count should
 | 
					
						
							|  |  |  |     // even out to 0.
 | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |     (expected as(Symbol | string)[]).forEach((nameOrSymbol) => { | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  |       const symbolName = typeof nameOrSymbol == 'string' ? nameOrSymbol : nameOrSymbol.name; | 
					
						
							|  |  |  |       diff[symbolName] = (diff[symbolName] || 0) + 1; | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     actual.forEach((s) => { | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  |       if (diff[s.name] === 1) { | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |         delete diff[s.name]; | 
					
						
							|  |  |  |       } else { | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  |         diff[s.name] = (diff[s.name] || 0) - 1; | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     return diff; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |   constructor(private path: string, private contents: string) { | 
					
						
							|  |  |  |     this.actual = SymbolExtractor.parse(path, contents); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   expect(expectedSymbols: (string|Symbol)[]) { | 
					
						
							|  |  |  |     expect(SymbolExtractor.diff(this.actual, expectedSymbols)).toEqual({}); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   compareAndPrintError(goldenFilePath: string, expected: string|((Symbol | string)[])): boolean { | 
					
						
							|  |  |  |     let passed = true; | 
					
						
							|  |  |  |     const diff = SymbolExtractor.diff(this.actual, expected); | 
					
						
							|  |  |  |     Object.keys(diff).forEach((key) => { | 
					
						
							|  |  |  |       if (passed) { | 
					
						
							|  |  |  |         console.error(`Expected symbols in '${this.path}' did not match gold file.`); | 
					
						
							|  |  |  |         passed = false; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  |       const missingOrExtra = diff[key] > 0 ? 'extra' : 'missing'; | 
					
						
							|  |  |  |       const count = Math.abs(diff[key]); | 
					
						
							|  |  |  |       console.error(`   Symbol: ${key} => ${count} ${missingOrExtra} in golden file.`); | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return passed; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-12 00:59:48 -08:00
										 |  |  | function stripSuffix(text: string): string { | 
					
						
							|  |  |  |   const index = text.lastIndexOf('$'); | 
					
						
							|  |  |  |   return index > -1 ? text.substring(0, index) : text; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  | /** | 
					
						
							| 
									
										
										
										
											2018-06-20 16:00:18 -07:00
										 |  |  |  * Detects if VariableDeclarationList is format `var ..., bundle = function(){}()`; | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |  * | 
					
						
							|  |  |  |  * Rollup produces this format when it wants to export symbols from a bundle. | 
					
						
							|  |  |  |  * @param child | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2018-06-20 16:00:18 -07:00
										 |  |  | function isRollupExportSymbol(decl: ts.VariableDeclaration): boolean { | 
					
						
							|  |  |  |   return !!(decl.initializer && decl.initializer.kind == ts.SyntaxKind.CallExpression) && | 
					
						
							|  |  |  |       ts.isIdentifier(decl.name) && decl.name.text === 'bundle'; | 
					
						
							| 
									
										
										
										
											2019-01-30 15:29:09 -08:00
										 |  |  | } |