| 
									
										
										
										
											2017-09-19 11:43:34 -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 fs from 'fs'; | 
					
						
							|  |  |  | import * as path from 'path'; | 
					
						
							|  |  |  | import * as ts from 'typescript'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import * as ng from '../index'; | 
					
						
							|  |  |  | import {FileChangeEvent, performWatchCompilation} from '../src/perform_watch'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {TestSupport, expectNoDiagnostics, setup} from './test_support'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | describe('perform watch', () => { | 
					
						
							|  |  |  |   let testSupport: TestSupport; | 
					
						
							|  |  |  |   let outDir: string; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   beforeEach(() => { | 
					
						
							|  |  |  |     testSupport = setup(); | 
					
						
							|  |  |  |     outDir = path.resolve(testSupport.basePath, 'outDir'); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function createConfig(): ng.ParsedConfiguration { | 
					
						
							|  |  |  |     const options = testSupport.createCompilerOptions({outDir}); | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       options, | 
					
						
							|  |  |  |       rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], | 
					
						
							|  |  |  |       project: path.resolve(testSupport.basePath, 'src/tsconfig.json'), | 
					
						
							|  |  |  |       emitFlags: ng.EmitFlags.Default, | 
					
						
							|  |  |  |       errors: [] | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   it('should compile files during the initial run', () => { | 
					
						
							|  |  |  |     const config = createConfig(); | 
					
						
							|  |  |  |     const host = new MockWatchHost(config); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     testSupport.writeFiles({ | 
					
						
							|  |  |  |       'src/main.ts': createModuleAndCompSource('main'), | 
					
						
							|  |  |  |       'src/index.ts': `export * from './main'; `, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const watchResult = performWatchCompilation(host); | 
					
						
							|  |  |  |     expectNoDiagnostics(config.options, watchResult.firstCompileResult); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     expect(fs.existsSync(path.resolve(outDir, 'src', 'main.ngfactory.js'))).toBe(true); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   it('should cache files on subsequent runs', () => { | 
					
						
							|  |  |  |     const config = createConfig(); | 
					
						
							|  |  |  |     const host = new MockWatchHost(config); | 
					
						
							|  |  |  |     let fileExistsSpy: jasmine.Spy; | 
					
						
							|  |  |  |     let getSourceFileSpy: jasmine.Spy; | 
					
						
							|  |  |  |     host.createCompilerHost = (options: ng.CompilerOptions) => { | 
					
						
							|  |  |  |       const ngHost = ng.createCompilerHost({options}); | 
					
						
							|  |  |  |       fileExistsSpy = spyOn(ngHost, 'fileExists').and.callThrough(); | 
					
						
							|  |  |  |       getSourceFileSpy = spyOn(ngHost, 'getSourceFile').and.callThrough(); | 
					
						
							|  |  |  |       return ngHost; | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     testSupport.writeFiles({ | 
					
						
							|  |  |  |       'src/main.ts': createModuleAndCompSource('main'), | 
					
						
							|  |  |  |       'src/util.ts': `export const x = 1;`, | 
					
						
							|  |  |  |       'src/index.ts': `
 | 
					
						
							|  |  |  |         export * from './main'; | 
					
						
							|  |  |  |         export * from './util'; | 
					
						
							|  |  |  |       `,
 | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const mainTsPath = path.resolve(testSupport.basePath, 'src', 'main.ts'); | 
					
						
							|  |  |  |     const utilTsPath = path.resolve(testSupport.basePath, 'src', 'util.ts'); | 
					
						
							|  |  |  |     const mainNgFactory = path.resolve(outDir, 'src', 'main.ngfactory.js'); | 
					
						
							|  |  |  |     performWatchCompilation(host); | 
					
						
							|  |  |  |     expect(fs.existsSync(mainNgFactory)).toBe(true); | 
					
						
							|  |  |  |     expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath); | 
					
						
							|  |  |  |     expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); | 
					
						
							|  |  |  |     expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); | 
					
						
							|  |  |  |     expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     fileExistsSpy !.calls.reset(); | 
					
						
							|  |  |  |     getSourceFileSpy !.calls.reset(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // trigger a single file change
 | 
					
						
							|  |  |  |     // -> all other files should be cached
 | 
					
						
							|  |  |  |     host.triggerFileChange(FileChangeEvent.Change, utilTsPath); | 
					
						
							| 
									
										
										
										
											2017-09-29 15:02:11 -07:00
										 |  |  |     expectNoDiagnostics(config.options, host.diagnostics); | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     expect(fileExistsSpy !).not.toHaveBeenCalledWith(mainTsPath); | 
					
						
							|  |  |  |     expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); | 
					
						
							|  |  |  |     expect(getSourceFileSpy !).not.toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); | 
					
						
							|  |  |  |     expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // trigger a folder change
 | 
					
						
							|  |  |  |     // -> nothing should be cached
 | 
					
						
							|  |  |  |     host.triggerFileChange( | 
					
						
							|  |  |  |         FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src')); | 
					
						
							| 
									
										
										
										
											2017-09-29 15:02:11 -07:00
										 |  |  |     expectNoDiagnostics(config.options, host.diagnostics); | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath); | 
					
						
							|  |  |  |     expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath); | 
					
						
							|  |  |  |     expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5); | 
					
						
							|  |  |  |     expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   it('should recover from static analysis errors', () => { | 
					
						
							|  |  |  |     const config = createConfig(); | 
					
						
							|  |  |  |     const host = new MockWatchHost(config); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const okFileContent = `
 | 
					
						
							|  |  |  |       import {NgModule} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       @NgModule() | 
					
						
							|  |  |  |       export class MyModule {} | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |     const errorFileContent = `
 | 
					
						
							|  |  |  |       import {NgModule} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-11-15 22:33:30 -08:00
										 |  |  |       @NgModule((() => (1===1 ? null as any : null as any)) as any) | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  |       export class MyModule {} | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |     const indexTsPath = path.resolve(testSupport.basePath, 'src', 'index.ts'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     testSupport.write(indexTsPath, okFileContent); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     performWatchCompilation(host); | 
					
						
							|  |  |  |     expectNoDiagnostics(config.options, host.diagnostics); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Do it multiple times as the watch mode switches internal modes.
 | 
					
						
							|  |  |  |     // E.g. from regular compile to using summaries, ...
 | 
					
						
							|  |  |  |     for (let i = 0; i < 3; i++) { | 
					
						
							|  |  |  |       host.diagnostics = []; | 
					
						
							|  |  |  |       testSupport.write(indexTsPath, okFileContent); | 
					
						
							|  |  |  |       host.triggerFileChange(FileChangeEvent.Change, indexTsPath); | 
					
						
							|  |  |  |       expectNoDiagnostics(config.options, host.diagnostics); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       host.diagnostics = []; | 
					
						
							|  |  |  |       testSupport.write(indexTsPath, errorFileContent); | 
					
						
							|  |  |  |       host.triggerFileChange(FileChangeEvent.Change, indexTsPath); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const errDiags = host.diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); | 
					
						
							|  |  |  |       expect(errDiags.length).toBe(1); | 
					
						
							| 
									
										
										
										
											2017-11-14 17:49:47 -08:00
										 |  |  |       expect(errDiags[0].messageText).toContain('Function expressions are not supported'); | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') { | 
					
						
							|  |  |  |   const templateEntry = | 
					
						
							|  |  |  |       template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``; | 
					
						
							|  |  |  |   return `
 | 
					
						
							|  |  |  |     import {Component, NgModule} from '@angular/core'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @Component({selector: '${prefix}', ${templateEntry}}) | 
					
						
							|  |  |  |     export class ${prefix}Comp {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @NgModule({declarations: [${prefix}Comp]}) | 
					
						
							|  |  |  |     export class ${prefix}Module {} | 
					
						
							|  |  |  |   `;
 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class MockWatchHost { | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  |   nextTimeoutListenerId = 1; | 
					
						
							|  |  |  |   timeoutListeners: {[id: string]: (() => void)} = {}; | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |   fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = []; | 
					
						
							| 
									
										
										
										
											2017-12-22 09:36:47 -08:00
										 |  |  |   diagnostics: ng.Diagnostic[] = []; | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |   constructor(public config: ng.ParsedConfiguration) {} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-22 09:36:47 -08:00
										 |  |  |   reportDiagnostics(diags: ng.Diagnostics) { this.diagnostics.push(...(diags as ng.Diagnostic[])); } | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |   readConfiguration() { return this.config; } | 
					
						
							| 
									
										
										
										
											2017-09-22 19:51:03 +02:00
										 |  |  |   createCompilerHost(options: ng.CompilerOptions) { return ng.createCompilerHost({options}); } | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |   createEmitCallback() { return undefined; } | 
					
						
							|  |  |  |   onFileChange( | 
					
						
							|  |  |  |       options: ng.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void, | 
					
						
							|  |  |  |       ready: () => void) { | 
					
						
							|  |  |  |     const id = this.fileChangeListeners.length; | 
					
						
							|  |  |  |     this.fileChangeListeners.push(listener); | 
					
						
							|  |  |  |     ready(); | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       close: () => this.fileChangeListeners[id] = null, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  |   setTimeout(callback: () => void): any { | 
					
						
							|  |  |  |     const id = this.nextTimeoutListenerId++; | 
					
						
							|  |  |  |     this.timeoutListeners[id] = callback; | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |     return id; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  |   clearTimeout(timeoutId: any): void { delete this.timeoutListeners[timeoutId]; } | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |   flushTimeouts() { | 
					
						
							| 
									
										
										
										
											2017-10-26 15:24:54 -07:00
										 |  |  |     const listeners = this.timeoutListeners; | 
					
						
							|  |  |  |     this.timeoutListeners = {}; | 
					
						
							|  |  |  |     Object.keys(listeners).forEach(id => listeners[id]()); | 
					
						
							| 
									
										
										
										
											2017-09-19 11:43:34 -07:00
										 |  |  |   } | 
					
						
							|  |  |  |   triggerFileChange(event: FileChangeEvent, fileName: string) { | 
					
						
							|  |  |  |     this.fileChangeListeners.forEach(listener => { | 
					
						
							|  |  |  |       if (listener) { | 
					
						
							|  |  |  |         listener(event, fileName); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     this.flushTimeouts(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |