Update the license headers throughout the repository to reference Google LLC rather than Google Inc, for the required license headers. PR Close #37205
		
			
				
	
	
		
			301 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC 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 chokidar from 'chokidar';
 | |
| import * as path from 'path';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| import {Diagnostics, exitCodeFromResult, ParsedConfiguration, performCompilation, PerformCompilationResult, readConfiguration} from './perform_compile';
 | |
| import * as api from './transformers/api';
 | |
| import {createCompilerHost} from './transformers/entry_points';
 | |
| import {createMessageDiagnostic} from './transformers/util';
 | |
| 
 | |
| function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic {
 | |
|   let duration: string;
 | |
|   if (timeInMillis > 1000) {
 | |
|     duration = `${(timeInMillis / 1000).toPrecision(2)}s`;
 | |
|   } else {
 | |
|     duration = `${timeInMillis}ms`;
 | |
|   }
 | |
|   return {
 | |
|     category: ts.DiagnosticCategory.Message,
 | |
|     messageText: `Total time: ${duration}`,
 | |
|     code: api.DEFAULT_ERROR_CODE,
 | |
|     source: api.SOURCE,
 | |
|   };
 | |
| }
 | |
| 
 | |
| export enum FileChangeEvent {
 | |
|   Change,
 | |
|   CreateDelete,
 | |
|   CreateDeleteDir,
 | |
| }
 | |
| 
 | |
| export interface PerformWatchHost {
 | |
|   reportDiagnostics(diagnostics: Diagnostics): void;
 | |
|   readConfiguration(): ParsedConfiguration;
 | |
|   createCompilerHost(options: api.CompilerOptions): api.CompilerHost;
 | |
|   createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined;
 | |
|   onFileChange(
 | |
|       options: api.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void,
 | |
|       ready: () => void): {close: () => void};
 | |
|   setTimeout(callback: () => void, ms: number): any;
 | |
|   clearTimeout(timeoutId: any): void;
 | |
| }
 | |
| 
 | |
| export function createPerformWatchHost(
 | |
|     configFileName: string, reportDiagnostics: (diagnostics: Diagnostics) => void,
 | |
|     existingOptions?: ts.CompilerOptions,
 | |
|     createEmitCallback?: (options: api.CompilerOptions) =>
 | |
|         api.TsEmitCallback | undefined): PerformWatchHost {
 | |
|   return {
 | |
|     reportDiagnostics: reportDiagnostics,
 | |
|     createCompilerHost: options => createCompilerHost({options}),
 | |
|     readConfiguration: () => readConfiguration(configFileName, existingOptions),
 | |
|     createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined,
 | |
|     onFileChange: (options, listener, ready: () => void) => {
 | |
|       if (!options.basePath) {
 | |
|         reportDiagnostics([{
 | |
|           category: ts.DiagnosticCategory.Error,
 | |
|           messageText: 'Invalid configuration option. baseDir not specified',
 | |
|           source: api.SOURCE,
 | |
|           code: api.DEFAULT_ERROR_CODE
 | |
|         }]);
 | |
|         return {close: () => {}};
 | |
|       }
 | |
|       const watcher = chokidar.watch(options.basePath, {
 | |
|         // ignore .dotfiles, .js and .map files.
 | |
|         // can't ignore other files as we e.g. want to recompile if an `.html` file changes as well.
 | |
|         ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json|node_modules)/,
 | |
|         ignoreInitial: true,
 | |
|         persistent: true,
 | |
|       });
 | |
|       watcher.on('all', (event: string, path: string) => {
 | |
|         switch (event) {
 | |
|           case 'change':
 | |
|             listener(FileChangeEvent.Change, path);
 | |
|             break;
 | |
|           case 'unlink':
 | |
|           case 'add':
 | |
|             listener(FileChangeEvent.CreateDelete, path);
 | |
|             break;
 | |
|           case 'unlinkDir':
 | |
|           case 'addDir':
 | |
|             listener(FileChangeEvent.CreateDeleteDir, path);
 | |
|             break;
 | |
|         }
 | |
|       });
 | |
|       watcher.on('ready', ready);
 | |
|       return {close: () => watcher.close(), ready};
 | |
|     },
 | |
|     setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout,
 | |
|     clearTimeout: (ts.sys.setTimeout && ts.sys.clearTimeout) || clearTimeout,
 | |
|   };
 | |
| }
 | |
| 
 | |
| interface CacheEntry {
 | |
|   exists?: boolean;
 | |
|   sf?: ts.SourceFile;
 | |
|   content?: string;
 | |
| }
 | |
| 
 | |
| interface QueuedCompilationInfo {
 | |
|   timerHandle: any;
 | |
|   modifiedResourceFiles: Set<string>;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * The logic in this function is adapted from `tsc.ts` from TypeScript.
 | |
|  */
 | |
| export function performWatchCompilation(host: PerformWatchHost):
 | |
|     {close: () => void, ready: (cb: () => void) => void, firstCompileResult: Diagnostics} {
 | |
|   let cachedProgram: api.Program|undefined;            // Program cached from last compilation
 | |
|   let cachedCompilerHost: api.CompilerHost|undefined;  // CompilerHost cached from last compilation
 | |
|   let cachedOptions: ParsedConfiguration|undefined;  // CompilerOptions cached from last compilation
 | |
|   let timerHandleForRecompilation: QueuedCompilationInfo|
 | |
|       undefined;  // Handle for 0.25s wait timer to trigger recompilation
 | |
| 
 | |
|   const ignoreFilesForWatch = new Set<string>();
 | |
|   const fileCache = new Map<string, CacheEntry>();
 | |
| 
 | |
|   const firstCompileResult = doCompilation();
 | |
| 
 | |
|   // Watch basePath, ignoring .dotfiles
 | |
|   let resolveReadyPromise: () => void;
 | |
|   const readyPromise = new Promise(resolve => resolveReadyPromise = resolve);
 | |
|   // Note: ! is ok as options are filled after the first compilation
 | |
|   // Note: ! is ok as resolvedReadyPromise is filled by the previous call
 | |
|   const fileWatcher =
 | |
|       host.onFileChange(cachedOptions!.options, watchedFileChanged, resolveReadyPromise!);
 | |
| 
 | |
|   return {close, ready: cb => readyPromise.then(cb), firstCompileResult};
 | |
| 
 | |
|   function cacheEntry(fileName: string): CacheEntry {
 | |
|     fileName = path.normalize(fileName);
 | |
|     let entry = fileCache.get(fileName);
 | |
|     if (!entry) {
 | |
|       entry = {};
 | |
|       fileCache.set(fileName, entry);
 | |
|     }
 | |
|     return entry;
 | |
|   }
 | |
| 
 | |
|   function close() {
 | |
|     fileWatcher.close();
 | |
|     if (timerHandleForRecompilation) {
 | |
|       host.clearTimeout(timerHandleForRecompilation.timerHandle);
 | |
|       timerHandleForRecompilation = undefined;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Invoked to perform initial compilation or re-compilation in watch mode
 | |
|   function doCompilation(): Diagnostics {
 | |
|     if (!cachedOptions) {
 | |
|       cachedOptions = host.readConfiguration();
 | |
|     }
 | |
|     if (cachedOptions.errors && cachedOptions.errors.length) {
 | |
|       host.reportDiagnostics(cachedOptions.errors);
 | |
|       return cachedOptions.errors;
 | |
|     }
 | |
|     const startTime = Date.now();
 | |
|     if (!cachedCompilerHost) {
 | |
|       cachedCompilerHost = host.createCompilerHost(cachedOptions.options);
 | |
|       const originalWriteFileCallback = cachedCompilerHost.writeFile;
 | |
|       cachedCompilerHost.writeFile = function(
 | |
|           fileName: string, data: string, writeByteOrderMark: boolean,
 | |
|           onError?: (message: string) => void, sourceFiles: ReadonlyArray<ts.SourceFile> = []) {
 | |
|         ignoreFilesForWatch.add(path.normalize(fileName));
 | |
|         return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles);
 | |
|       };
 | |
|       const originalFileExists = cachedCompilerHost.fileExists;
 | |
|       cachedCompilerHost.fileExists = function(fileName: string) {
 | |
|         const ce = cacheEntry(fileName);
 | |
|         if (ce.exists == null) {
 | |
|           ce.exists = originalFileExists.call(this, fileName);
 | |
|         }
 | |
|         return ce.exists!;
 | |
|       };
 | |
|       const originalGetSourceFile = cachedCompilerHost.getSourceFile;
 | |
|       cachedCompilerHost.getSourceFile = function(
 | |
|           fileName: string, languageVersion: ts.ScriptTarget) {
 | |
|         const ce = cacheEntry(fileName);
 | |
|         if (!ce.sf) {
 | |
|           ce.sf = originalGetSourceFile.call(this, fileName, languageVersion);
 | |
|         }
 | |
|         return ce.sf!;
 | |
|       };
 | |
|       const originalReadFile = cachedCompilerHost.readFile;
 | |
|       cachedCompilerHost.readFile = function(fileName: string) {
 | |
|         const ce = cacheEntry(fileName);
 | |
|         if (ce.content == null) {
 | |
|           ce.content = originalReadFile.call(this, fileName);
 | |
|         }
 | |
|         return ce.content!;
 | |
|       };
 | |
|       // Provide access to the file paths that triggered this rebuild
 | |
|       cachedCompilerHost.getModifiedResourceFiles = function() {
 | |
|         if (timerHandleForRecompilation === undefined) {
 | |
|           return undefined;
 | |
|         }
 | |
|         return timerHandleForRecompilation.modifiedResourceFiles;
 | |
|       };
 | |
|     }
 | |
|     ignoreFilesForWatch.clear();
 | |
|     const oldProgram = cachedProgram;
 | |
|     // We clear out the `cachedProgram` here as a
 | |
|     // program can only be used as `oldProgram` 1x
 | |
|     cachedProgram = undefined;
 | |
|     const compileResult = performCompilation({
 | |
|       rootNames: cachedOptions.rootNames,
 | |
|       options: cachedOptions.options,
 | |
|       host: cachedCompilerHost,
 | |
|       oldProgram: oldProgram,
 | |
|       emitCallback: host.createEmitCallback(cachedOptions.options)
 | |
|     });
 | |
| 
 | |
|     if (compileResult.diagnostics.length) {
 | |
|       host.reportDiagnostics(compileResult.diagnostics);
 | |
|     }
 | |
| 
 | |
|     const endTime = Date.now();
 | |
|     if (cachedOptions.options.diagnostics) {
 | |
|       const totalTime = (endTime - startTime) / 1000;
 | |
|       host.reportDiagnostics([totalCompilationTimeDiagnostic(endTime - startTime)]);
 | |
|     }
 | |
|     const exitCode = exitCodeFromResult(compileResult.diagnostics);
 | |
|     if (exitCode == 0) {
 | |
|       cachedProgram = compileResult.program;
 | |
|       host.reportDiagnostics(
 | |
|           [createMessageDiagnostic('Compilation complete. Watching for file changes.')]);
 | |
|     } else {
 | |
|       host.reportDiagnostics(
 | |
|           [createMessageDiagnostic('Compilation failed. Watching for file changes.')]);
 | |
|     }
 | |
| 
 | |
|     return compileResult.diagnostics;
 | |
|   }
 | |
| 
 | |
|   function resetOptions() {
 | |
|     cachedProgram = undefined;
 | |
|     cachedCompilerHost = undefined;
 | |
|     cachedOptions = undefined;
 | |
|   }
 | |
| 
 | |
|   function watchedFileChanged(event: FileChangeEvent, fileName: string) {
 | |
|     const normalizedPath = path.normalize(fileName);
 | |
| 
 | |
|     if (cachedOptions && event === FileChangeEvent.Change &&
 | |
|         // TODO(chuckj): validate that this is sufficient to skip files that were written.
 | |
|         // This assumes that the file path we write is the same file path we will receive in the
 | |
|         // change notification.
 | |
|         normalizedPath === path.normalize(cachedOptions.project)) {
 | |
|       // If the configuration file changes, forget everything and start the recompilation timer
 | |
|       resetOptions();
 | |
|     } else if (
 | |
|         event === FileChangeEvent.CreateDelete || event === FileChangeEvent.CreateDeleteDir) {
 | |
|       // If a file was added or removed, reread the configuration
 | |
|       // to determine the new list of root files.
 | |
|       cachedOptions = undefined;
 | |
|     }
 | |
| 
 | |
|     if (event === FileChangeEvent.CreateDeleteDir) {
 | |
|       fileCache.clear();
 | |
|     } else {
 | |
|       fileCache.delete(normalizedPath);
 | |
|     }
 | |
| 
 | |
|     if (!ignoreFilesForWatch.has(normalizedPath)) {
 | |
|       // Ignore the file if the file is one that was written by the compiler.
 | |
|       startTimerForRecompilation(normalizedPath);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
 | |
|   // operations (such as saving all modified files in an editor) a chance to complete before we kick
 | |
|   // off a new compilation.
 | |
|   function startTimerForRecompilation(changedPath: string) {
 | |
|     if (timerHandleForRecompilation) {
 | |
|       host.clearTimeout(timerHandleForRecompilation.timerHandle);
 | |
|     } else {
 | |
|       timerHandleForRecompilation = {
 | |
|         modifiedResourceFiles: new Set<string>(),
 | |
|         timerHandle: undefined
 | |
|       };
 | |
|     }
 | |
|     timerHandleForRecompilation.timerHandle = host.setTimeout(recompile, 250);
 | |
|     timerHandleForRecompilation.modifiedResourceFiles.add(changedPath);
 | |
|   }
 | |
| 
 | |
|   function recompile() {
 | |
|     host.reportDiagnostics(
 | |
|         [createMessageDiagnostic('File change detected. Starting incremental compilation.')]);
 | |
|     doCompilation();
 | |
|     timerHandleForRecompilation = undefined;
 | |
|   }
 | |
| }
 |