| 
									
										
										
										
											2017-07-13 14:25:17 -07: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'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-30 17:17:54 -08:00
										 |  |  | import {MetadataCollector, ModuleMetadata} from '../../src/metadata/index'; | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  | import {getExpressionLoweringTransformFactory, LoweringRequest, LowerMetadataTransform, RequestLocationMap} from '../../src/transformers/lower_expressions'; | 
					
						
							| 
									
										
										
										
											2018-01-30 17:17:54 -08:00
										 |  |  | import {MetadataCache} from '../../src/transformers/metadata_cache'; | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  | import {Directory, MockAotContext, MockCompilerHost} from '../mocks'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 08:45:21 -08:00
										 |  |  | const DEFAULT_FIELDS_TO_LOWER = ['useFactory', 'useValue', 'data']; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  | describe('Expression lowering', () => { | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |   describe('transform', () => { | 
					
						
							|  |  |  |     it('should be able to lower a simple expression', () => { | 
					
						
							|  |  |  |       expect(convert('const a = 1 +◊b: 2◊;')).toBe('const b = 2; const a = 1 + b; export { b };'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should be able to lower an expression in a decorator', () => { | 
					
						
							|  |  |  |       expect(convert(`
 | 
					
						
							|  |  |  |           import {Component} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           @Component({ | 
					
						
							|  |  |  |             provider: [{provide: 'someToken', useFactory:◊l: () => null◊}] | 
					
						
							|  |  |  |           }) | 
					
						
							|  |  |  |           class MyClass {} | 
					
						
							|  |  |  |       `)).toContain('const l = () => null; exports.l = l;');
 | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2017-09-01 16:27:35 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('should be able to export a variable if the whole value is lowered', () => { | 
					
						
							|  |  |  |       expect(convert('/*a*/ const a =◊b: () => null◊;')) | 
					
						
							|  |  |  |           .toBe('/*a*/ const a = () => null; const b = a; export { b };'); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |   describe('collector', () => { | 
					
						
							|  |  |  |     it('should request a lowering for useValue', () => { | 
					
						
							|  |  |  |       const collected = collect(`
 | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |         import {Component} from '@angular/core'; | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         enum SomeEnum { | 
					
						
							|  |  |  |           OK, | 
					
						
							|  |  |  |           NotOK | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |         @Component({ | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |           provider: [{provide: 'someToken', useValue:◊enum: SomeEnum.OK◊}] | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |         }) | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |         export class MyClass {} | 
					
						
							|  |  |  |       `);
 | 
					
						
							|  |  |  |       expect(collected.requests.has(collected.annotations[0].start)) | 
					
						
							|  |  |  |           .toBeTruthy('did not find the useValue'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-20 16:26:11 -07:00
										 |  |  |     it('should not request a lowering for useValue with a reference to a static property', () => { | 
					
						
							|  |  |  |       const collected = collect(`
 | 
					
						
							|  |  |  |         import {Component} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         @Component({ | 
					
						
							|  |  |  |           provider: [{provide: 'someToken', useValue:◊value: MyClass.someMethod◊}] | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         export class MyClass { | 
					
						
							|  |  |  |           static someMethod() {} | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       `);
 | 
					
						
							|  |  |  |       expect(collected.requests.size).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |     it('should request a lowering for useFactory', () => { | 
					
						
							|  |  |  |       const collected = collect(`
 | 
					
						
							|  |  |  |         import {Component} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         @Component({ | 
					
						
							|  |  |  |           provider: [{provide: 'someToken', useFactory:◊lambda: () => null◊}] | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         export class MyClass {} | 
					
						
							|  |  |  |       `);
 | 
					
						
							|  |  |  |       expect(collected.requests.has(collected.annotations[0].start)) | 
					
						
							|  |  |  |           .toBeTruthy('did not find the useFactory'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should request a lowering for data', () => { | 
					
						
							|  |  |  |       const collected = collect(`
 | 
					
						
							|  |  |  |         import {Component} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         enum SomeEnum { | 
					
						
							|  |  |  |           OK, | 
					
						
							|  |  |  |           NotOK | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         @Component({ | 
					
						
							|  |  |  |           provider: [{provide: 'someToken', data:◊enum: SomeEnum.OK◊}] | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         export class MyClass {} | 
					
						
							|  |  |  |       `);
 | 
					
						
							|  |  |  |       expect(collected.requests.has(collected.annotations[0].start)) | 
					
						
							|  |  |  |           .toBeTruthy('did not find the data field'); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2017-09-25 12:36:08 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-18 14:36:49 -08:00
										 |  |  |     it('should not lower a non-module', () => { | 
					
						
							|  |  |  |       const collected = collect(`
 | 
					
						
							|  |  |  |           declare const global: any; | 
					
						
							|  |  |  |           const ngDevMode: boolean = (function(global: any) { | 
					
						
							|  |  |  |             return global.ngDevMode = true; | 
					
						
							|  |  |  |           })(typeof window != 'undefined' && window || typeof self != 'undefined' && self || typeof global != 'undefined' && global); | 
					
						
							|  |  |  |        `);
 | 
					
						
							|  |  |  |       expect(collected.requests.size).toBe(0, 'unexpected rewriting'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should throw a validation exception for invalid files', () => { | 
					
						
							| 
									
										
										
										
											2018-01-30 17:17:54 -08:00
										 |  |  |       const cache = new MetadataCache( | 
					
						
							| 
									
										
										
										
											2018-02-16 08:45:21 -08:00
										 |  |  |           new MetadataCollector({}), /* strict */ true, | 
					
						
							|  |  |  |           [new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER)]); | 
					
						
							| 
									
										
										
										
											2017-09-25 12:36:08 -07:00
										 |  |  |       const sourceFile = ts.createSourceFile( | 
					
						
							|  |  |  |           'foo.ts', `
 | 
					
						
							|  |  |  |         import {Injectable} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         class SomeLocalClass {} | 
					
						
							|  |  |  |         @Injectable() | 
					
						
							|  |  |  |         export class SomeClass { | 
					
						
							|  |  |  |           constructor(a: SomeLocalClass) {} | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       `,
 | 
					
						
							|  |  |  |           ts.ScriptTarget.Latest, true); | 
					
						
							|  |  |  |       expect(() => cache.getMetadata(sourceFile)).toThrow(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should not report validation errors on a .d.ts file', () => { | 
					
						
							| 
									
										
										
										
											2018-01-30 17:17:54 -08:00
										 |  |  |       const cache = new MetadataCache( | 
					
						
							| 
									
										
										
										
											2018-02-16 08:45:21 -08:00
										 |  |  |           new MetadataCollector({}), /* strict */ true, | 
					
						
							|  |  |  |           [new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER)]); | 
					
						
							| 
									
										
										
										
											2017-09-25 12:36:08 -07:00
										 |  |  |       const dtsFile = ts.createSourceFile( | 
					
						
							|  |  |  |           'foo.d.ts', `
 | 
					
						
							|  |  |  |         import {Injectable} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         class SomeLocalClass {} | 
					
						
							|  |  |  |         @Injectable() | 
					
						
							|  |  |  |         export class SomeClass { | 
					
						
							|  |  |  |           constructor(a: SomeLocalClass) {} | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       `,
 | 
					
						
							|  |  |  |           ts.ScriptTarget.Latest, true); | 
					
						
							|  |  |  |       expect(() => cache.getMetadata(dtsFile)).not.toThrow(); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  | // Helpers
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface Annotation { | 
					
						
							|  |  |  |   start: number; | 
					
						
							|  |  |  |   length: number; | 
					
						
							|  |  |  |   name: string; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getAnnotations(annotatedSource: string): | 
					
						
							|  |  |  |     {unannotatedSource: string, annotations: Annotation[]} { | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |   const annotations: {start: number, length: number, name: string}[] = []; | 
					
						
							|  |  |  |   let adjustment = 0; | 
					
						
							|  |  |  |   const unannotatedSource = annotatedSource.replace( | 
					
						
							|  |  |  |       /◊([a-zA-Z]+):(.*)◊/g, | 
					
						
							|  |  |  |       (text: string, name: string, source: string, index: number): string => { | 
					
						
							|  |  |  |         annotations.push({start: index + adjustment, length: source.length, name}); | 
					
						
							|  |  |  |         adjustment -= text.length - source.length; | 
					
						
							|  |  |  |         return source; | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |   return {unannotatedSource, annotations}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Transform helpers
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function convert(annotatedSource: string) { | 
					
						
							|  |  |  |   const {annotations, unannotatedSource} = getAnnotations(annotatedSource); | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const baseFileName = 'someFile'; | 
					
						
							|  |  |  |   const moduleName = '/' + baseFileName; | 
					
						
							|  |  |  |   const fileName = moduleName + '.ts'; | 
					
						
							|  |  |  |   const context = new MockAotContext('/', {[baseFileName + '.ts']: unannotatedSource}); | 
					
						
							|  |  |  |   const host = new MockCompilerHost(context); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const sourceFile = ts.createSourceFile( | 
					
						
							|  |  |  |       fileName, unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true); | 
					
						
							|  |  |  |   const requests = new Map<number, LoweringRequest>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   for (const annotation of annotations) { | 
					
						
							|  |  |  |     const node = findNode(sourceFile, annotation.start, annotation.length); | 
					
						
							| 
									
										
										
										
											2017-08-08 12:40:08 -07:00
										 |  |  |     if (!node) throw new Error('Invalid test specification. Could not find the node to substitute'); | 
					
						
							|  |  |  |     const location = node.pos; | 
					
						
							|  |  |  |     requests.set(location, {name: annotation.name, kind: node.kind, location, end: node.end}); | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const program = ts.createProgram( | 
					
						
							|  |  |  |       [fileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host); | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |   const moduleSourceFile = program.getSourceFile(fileName)!; | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |   const transformers: ts.CustomTransformers = { | 
					
						
							| 
									
										
										
										
											2017-10-24 11:26:04 -07:00
										 |  |  |     before: [getExpressionLoweringTransformFactory( | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |           getRequests(sourceFile: ts.SourceFile): RequestLocationMap { | 
					
						
							| 
									
										
										
										
											2017-10-24 11:26:04 -07:00
										 |  |  |             if (sourceFile.fileName == moduleSourceFile.fileName) { | 
					
						
							|  |  |  |               return requests; | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |             } else { | 
					
						
							|  |  |  |               return new Map(); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2017-10-24 11:26:04 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         program)] | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  |   let result: string = ''; | 
					
						
							|  |  |  |   const emitResult = program.emit( | 
					
						
							|  |  |  |       moduleSourceFile, (emittedFileName, data, writeByteOrderMark, onError, sourceFiles) => { | 
					
						
							|  |  |  |         if (fileName.startsWith(moduleName)) { | 
					
						
							|  |  |  |           result = data; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }, undefined, undefined, transformers); | 
					
						
							|  |  |  |   return normalizeResult(result); | 
					
						
							| 
									
										
										
										
											2017-09-22 19:51:03 +02:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2017-07-13 14:25:17 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | function findNode(node: ts.Node, start: number, length: number): ts.Node|undefined { | 
					
						
							|  |  |  |   function find(node: ts.Node): ts.Node|undefined { | 
					
						
							|  |  |  |     if (node.getFullStart() == start && node.getEnd() == start + length) { | 
					
						
							|  |  |  |       return node; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (node.getFullStart() <= start && node.getEnd() >= start + length) { | 
					
						
							|  |  |  |       return ts.forEachChild(node, find); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return ts.forEachChild(node, find); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function normalizeResult(result: string): string { | 
					
						
							|  |  |  |   // Remove TypeScript prefixes
 | 
					
						
							|  |  |  |   // Remove new lines
 | 
					
						
							|  |  |  |   // Squish adjacent spaces
 | 
					
						
							|  |  |  |   // Remove prefix and postfix spaces
 | 
					
						
							|  |  |  |   return result.replace('"use strict";', ' ') | 
					
						
							|  |  |  |       .replace('exports.__esModule = true;', ' ') | 
					
						
							|  |  |  |       .replace('Object.defineProperty(exports, "__esModule", { value: true });', ' ') | 
					
						
							|  |  |  |       .replace(/\n/g, ' ') | 
					
						
							|  |  |  |       .replace(/ +/g, ' ') | 
					
						
							|  |  |  |       .replace(/^ /g, '') | 
					
						
							|  |  |  |       .replace(/ $/g, ''); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | // Collector helpers
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function collect(annotatedSource: string) { | 
					
						
							|  |  |  |   const {annotations, unannotatedSource} = getAnnotations(annotatedSource); | 
					
						
							| 
									
										
										
										
											2018-02-16 08:45:21 -08:00
										 |  |  |   const transformer = new LowerMetadataTransform(DEFAULT_FIELDS_TO_LOWER); | 
					
						
							| 
									
										
										
										
											2018-01-30 17:17:54 -08:00
										 |  |  |   const cache = new MetadataCache(new MetadataCollector({}), false, [transformer]); | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |   const sourceFile = ts.createSourceFile( | 
					
						
							|  |  |  |       'someName.ts', unannotatedSource, ts.ScriptTarget.Latest, /* setParentNodes */ true); | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     metadata: cache.getMetadata(sourceFile), | 
					
						
							| 
									
										
										
										
											2020-04-07 12:43:43 -07:00
										 |  |  |     requests: transformer.getRequests(sourceFile), | 
					
						
							|  |  |  |     annotations | 
					
						
							| 
									
										
										
										
											2017-08-23 10:22:17 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  | } |