Don't use `Array` constructor with the size value (ex. `new Array(5)`) - this will create a `HOLEY_ELEMENTS` array (even if this array is filled in later on!); https://v8.dev/blog/elements-kinds https://stackoverflow.com/questions/32054170/how-to-resize-an-array PR Close #32155
		
			
				
	
	
		
			855 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			855 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @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 {AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler, toTypeScript} from '@angular/compiler';
 | |
| import {MetadataBundlerHost} from '@angular/compiler-cli/src/metadata/bundler';
 | |
| import {MetadataCollector} from '@angular/compiler-cli/src/metadata/collector';
 | |
| import {ModuleMetadata} from '@angular/compiler-cli/src/metadata/index';
 | |
| import {newArray} from '@angular/compiler/src/util';
 | |
| import * as fs from 'fs';
 | |
| import * as path from 'path';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; }
 | |
| 
 | |
| let nodeModulesPath: string;
 | |
| let angularSourcePath: string;
 | |
| let rootPath: string;
 | |
| 
 | |
| calcPathsOnDisc();
 | |
| 
 | |
| export type MockFileOrDirectory = string | MockDirectory;
 | |
| 
 | |
| export type MockDirectory = {
 | |
|   [name: string]: MockFileOrDirectory | undefined;
 | |
| };
 | |
| 
 | |
| export function isDirectory(data: MockFileOrDirectory | undefined): data is MockDirectory {
 | |
|   return typeof data !== 'string';
 | |
| }
 | |
| 
 | |
| const rxjs = /\/rxjs\//;
 | |
| export const settings: ts.CompilerOptions = {
 | |
|   target: ts.ScriptTarget.ES5,
 | |
|   declaration: true,
 | |
|   module: ts.ModuleKind.CommonJS,
 | |
|   moduleResolution: ts.ModuleResolutionKind.NodeJs,
 | |
|   emitDecoratorMetadata: true,
 | |
|   experimentalDecorators: true,
 | |
|   removeComments: false,
 | |
|   noImplicitAny: false,
 | |
|   skipLibCheck: true,
 | |
|   strictNullChecks: true,
 | |
|   lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
 | |
|   types: []
 | |
| };
 | |
| 
 | |
| export interface EmitterOptions {
 | |
|   emitMetadata: boolean;
 | |
|   mockData?: MockDirectory;
 | |
|   context?: Map<string, string>;
 | |
| }
 | |
| 
 | |
| function calcPathsOnDisc() {
 | |
|   const moduleFilename = module.filename.replace(/\\/g, '/');
 | |
|   const distIndex = moduleFilename.indexOf('/dist/all');
 | |
|   if (distIndex >= 0) {
 | |
|     rootPath = moduleFilename.substr(0, distIndex);
 | |
|     nodeModulesPath = path.join(rootPath, 'node_modules');
 | |
|     angularSourcePath = path.join(rootPath, 'packages');
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| export class EmittingCompilerHost implements ts.CompilerHost {
 | |
|   private addedFiles = new Map<string, string>();
 | |
|   private writtenFiles = new Map<string, string>();
 | |
|   private scriptNames: string[];
 | |
|   private root = '/';
 | |
|   private collector = new MetadataCollector();
 | |
|   private cachedAddedDirectories: Set<string>|undefined;
 | |
| 
 | |
|   constructor(scriptNames: string[], private options: EmitterOptions) {
 | |
|     // Rewrite references to scripts with '@angular' to its corresponding location in
 | |
|     // the source tree.
 | |
|     this.scriptNames = scriptNames.map(f => this.effectiveName(f));
 | |
|     this.root = rootPath || this.root;
 | |
|     if (options.context) {
 | |
|       this.addedFiles = mergeMaps(options.context);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   public writtenAngularFiles(target = new Map<string, string>()): Map<string, string> {
 | |
|     this.written.forEach((value, key) => {
 | |
|       const path = `/node_modules/@angular${key.substring(angularSourcePath.length)}`;
 | |
|       target.set(path, value);
 | |
|     });
 | |
|     return target;
 | |
|   }
 | |
| 
 | |
|   public addScript(fileName: string, content: string) {
 | |
|     const scriptName = this.effectiveName(fileName);
 | |
|     this.addedFiles.set(scriptName, content);
 | |
|     this.cachedAddedDirectories = undefined;
 | |
|     this.scriptNames.push(scriptName);
 | |
|   }
 | |
| 
 | |
|   public override(fileName: string, content: string) {
 | |
|     const scriptName = this.effectiveName(fileName);
 | |
|     this.addedFiles.set(scriptName, content);
 | |
|     this.cachedAddedDirectories = undefined;
 | |
|   }
 | |
| 
 | |
|   public addFiles(map: Map<string, string>) {
 | |
|     for (const [name, content] of Array.from(map.entries())) {
 | |
|       this.addedFiles.set(name, content);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   public addWrittenFile(fileName: string, content: string) {
 | |
|     this.writtenFiles.set(this.effectiveName(fileName), content);
 | |
|   }
 | |
| 
 | |
|   public getWrittenFiles(): {name: string, content: string}[] {
 | |
|     return Array.from(this.writtenFiles).map(f => ({name: f[0], content: f[1]}));
 | |
|   }
 | |
| 
 | |
|   public get scripts(): string[] { return this.scriptNames; }
 | |
| 
 | |
|   public get written(): Map<string, string> { return this.writtenFiles; }
 | |
| 
 | |
|   public effectiveName(fileName: string): string {
 | |
|     const prefix = '@angular/';
 | |
|     return angularSourcePath && fileName.startsWith(prefix) ?
 | |
|         path.join(angularSourcePath, fileName.substr(prefix.length)) :
 | |
|         fileName;
 | |
|   }
 | |
| 
 | |
|   // ts.ModuleResolutionHost
 | |
|   fileExists(fileName: string): boolean {
 | |
|     return this.addedFiles.has(fileName) || open(fileName, this.options.mockData) != null ||
 | |
|         fs.existsSync(fileName);
 | |
|   }
 | |
| 
 | |
|   readFile(fileName: string): string {
 | |
|     const result = this.addedFiles.get(fileName) || open(fileName, this.options.mockData);
 | |
|     if (result) return result;
 | |
| 
 | |
|     let basename = path.basename(fileName);
 | |
|     if (/^lib.*\.d\.ts$/.test(basename)) {
 | |
|       let libPath = ts.getDefaultLibFilePath(settings);
 | |
|       return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
 | |
|     }
 | |
|     return fs.readFileSync(fileName, 'utf8');
 | |
|   }
 | |
| 
 | |
|   directoryExists(directoryName: string): boolean {
 | |
|     return directoryExists(directoryName, this.options.mockData) ||
 | |
|         this.getAddedDirectories().has(directoryName) ||
 | |
|         (fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory());
 | |
|   }
 | |
| 
 | |
|   getCurrentDirectory(): string { return this.root; }
 | |
| 
 | |
|   getDirectories(dir: string): string[] {
 | |
|     const result = open(dir, this.options.mockData);
 | |
|     if (result && typeof result !== 'string') {
 | |
|       return Object.keys(result);
 | |
|     }
 | |
|     return fs.readdirSync(dir).filter(p => {
 | |
|       const name = path.join(dir, p);
 | |
|       const stat = fs.statSync(name);
 | |
|       return stat && stat.isDirectory();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // ts.CompilerHost
 | |
|   getSourceFile(
 | |
|       fileName: string, languageVersion: ts.ScriptTarget,
 | |
|       onError?: (message: string) => void): ts.SourceFile {
 | |
|     const content = this.readFile(fileName);
 | |
|     if (content) {
 | |
|       return ts.createSourceFile(fileName, content, languageVersion, /* setParentNodes */ true);
 | |
|     }
 | |
|     throw new Error(`File not found '${fileName}'.`);
 | |
|   }
 | |
| 
 | |
|   getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
 | |
| 
 | |
|   writeFile: ts.WriteFileCallback =
 | |
|       (fileName: string, data: string, writeByteOrderMark: boolean,
 | |
|        onError?: (message: string) => void, sourceFiles?: ReadonlyArray<ts.SourceFile>) => {
 | |
|         this.addWrittenFile(fileName, data);
 | |
|         if (this.options.emitMetadata && sourceFiles && sourceFiles.length && DTS.test(fileName)) {
 | |
|           const metadataFilePath = fileName.replace(DTS, '.metadata.json');
 | |
|           const metadata = this.collector.getMetadata(sourceFiles[0]);
 | |
|           if (metadata) {
 | |
|             this.addWrittenFile(metadataFilePath, JSON.stringify(metadata));
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|   getCanonicalFileName(fileName: string): string {
 | |
|     return fileName;
 | |
|   }
 | |
|   useCaseSensitiveFileNames(): boolean { return false; }
 | |
|   getNewLine(): string { return '\n'; }
 | |
| 
 | |
|   private getAddedDirectories(): Set<string> {
 | |
|     let result = this.cachedAddedDirectories;
 | |
|     if (!result) {
 | |
|       const newCache = new Set<string>();
 | |
|       const addFile = (fileName: string) => {
 | |
|         const directory = fileName.substr(0, fileName.lastIndexOf('/'));
 | |
|         if (!newCache.has(directory)) {
 | |
|           newCache.add(directory);
 | |
|           addFile(directory);
 | |
|         }
 | |
|       };
 | |
|       Array.from(this.addedFiles.keys()).forEach(addFile);
 | |
|       this.cachedAddedDirectories = result = newCache;
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| }
 | |
| 
 | |
| export class MockCompilerHost implements ts.CompilerHost {
 | |
|   scriptNames: string[];
 | |
| 
 | |
|   public overrides = new Map<string, string>();
 | |
|   public writtenFiles = new Map<string, string>();
 | |
|   private sourceFiles = new Map<string, ts.SourceFile>();
 | |
|   private assumeExists = new Set<string>();
 | |
|   private traces: string[] = [];
 | |
| 
 | |
|   constructor(scriptNames: string[], private data: MockDirectory) {
 | |
|     this.scriptNames = [...scriptNames];
 | |
|   }
 | |
| 
 | |
|   // Test API
 | |
|   override(fileName: string, content: string) {
 | |
|     if (content) {
 | |
|       this.overrides.set(fileName, content);
 | |
|     } else {
 | |
|       this.overrides.delete(fileName);
 | |
|     }
 | |
|     this.sourceFiles.delete(fileName);
 | |
|   }
 | |
| 
 | |
|   addScript(fileName: string, content: string) {
 | |
|     this.overrides.set(fileName, content);
 | |
|     this.scriptNames.push(fileName);
 | |
|     this.sourceFiles.delete(fileName);
 | |
|   }
 | |
| 
 | |
|   assumeFileExists(fileName: string) { this.assumeExists.add(fileName); }
 | |
| 
 | |
|   remove(files: string[]) {
 | |
|     // Remove the files from the list of scripts.
 | |
|     const fileSet = new Set(files);
 | |
|     this.scriptNames = this.scriptNames.filter(f => fileSet.has(f));
 | |
| 
 | |
|     // Remove files from written files
 | |
|     files.forEach(f => this.writtenFiles.delete(f));
 | |
|   }
 | |
| 
 | |
|   // ts.ModuleResolutionHost
 | |
|   fileExists(fileName: string): boolean {
 | |
|     if (this.overrides.has(fileName) || this.writtenFiles.has(fileName) ||
 | |
|         this.assumeExists.has(fileName)) {
 | |
|       return true;
 | |
|     }
 | |
|     const effectiveName = this.getEffectiveName(fileName);
 | |
|     if (effectiveName == fileName) {
 | |
|       return open(fileName, this.data) != null;
 | |
|     }
 | |
|     if (fileName.match(rxjs)) {
 | |
|       return fs.existsSync(effectiveName);
 | |
|     }
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   readFile(fileName: string): string { return this.getFileContent(fileName) !; }
 | |
| 
 | |
|   trace(s: string): void { this.traces.push(s); }
 | |
| 
 | |
|   getCurrentDirectory(): string { return '/'; }
 | |
| 
 | |
|   getDirectories(dir: string): string[] {
 | |
|     const effectiveName = this.getEffectiveName(dir);
 | |
|     if (effectiveName === dir) {
 | |
|       const data = find(dir, this.data);
 | |
|       if (isDirectory(data)) {
 | |
|         return Object.keys(data).filter(k => isDirectory(data[k]));
 | |
|       }
 | |
|     }
 | |
|     return [];
 | |
|   }
 | |
| 
 | |
|   // ts.CompilerHost
 | |
|   getSourceFile(
 | |
|       fileName: string, languageVersion: ts.ScriptTarget,
 | |
|       onError?: (message: string) => void): ts.SourceFile {
 | |
|     let result = this.sourceFiles.get(fileName);
 | |
|     if (!result) {
 | |
|       const content = this.getFileContent(fileName);
 | |
|       if (content) {
 | |
|         result = ts.createSourceFile(fileName, content, languageVersion);
 | |
|         this.sourceFiles.set(fileName, result);
 | |
|       }
 | |
|     }
 | |
|     return result !;
 | |
|   }
 | |
| 
 | |
|   getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
 | |
| 
 | |
|   writeFile: ts.WriteFileCallback =
 | |
|       (fileName: string, data: string, writeByteOrderMark: boolean) => {
 | |
|         this.writtenFiles.set(fileName, data);
 | |
|         this.sourceFiles.delete(fileName);
 | |
|       }
 | |
| 
 | |
|   getCanonicalFileName(fileName: string): string {
 | |
|     return fileName;
 | |
|   }
 | |
|   useCaseSensitiveFileNames(): boolean { return false; }
 | |
|   getNewLine(): string { return '\n'; }
 | |
| 
 | |
|   // Private methods
 | |
|   private getFileContent(fileName: string): string|undefined {
 | |
|     if (this.overrides.has(fileName)) {
 | |
|       return this.overrides.get(fileName);
 | |
|     }
 | |
|     if (this.writtenFiles.has(fileName)) {
 | |
|       return this.writtenFiles.get(fileName);
 | |
|     }
 | |
|     let basename = path.basename(fileName);
 | |
|     if (/^lib.*\.d\.ts$/.test(basename)) {
 | |
|       let libPath = ts.getDefaultLibFilePath(settings);
 | |
|       return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
 | |
|     }
 | |
|     let effectiveName = this.getEffectiveName(fileName);
 | |
|     if (effectiveName === fileName) {
 | |
|       return open(fileName, this.data);
 | |
|     }
 | |
|     if (fileName.match(rxjs) && fs.existsSync(fileName)) {
 | |
|       return fs.readFileSync(fileName, 'utf8');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private getEffectiveName(name: string): string {
 | |
|     const node_modules = 'node_modules';
 | |
|     const rxjs = '/rxjs';
 | |
|     if (name.startsWith('/' + node_modules)) {
 | |
|       if (nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
 | |
|         return path.join(nodeModulesPath, name.substr(node_modules.length + 1));
 | |
|       }
 | |
|     }
 | |
|     return name;
 | |
|   }
 | |
| }
 | |
| 
 | |
| const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
 | |
| const DTS = /\.d\.ts$/;
 | |
| const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$/;
 | |
| 
 | |
| export class MockAotCompilerHost implements AotCompilerHost {
 | |
|   private metadataVisible: boolean = true;
 | |
|   private dtsAreSource: boolean = true;
 | |
|   private resolveModuleNameHost: ts.ModuleResolutionHost;
 | |
| 
 | |
|   constructor(
 | |
|       private tsHost: MockCompilerHost,
 | |
|       private metadataProvider: MetadataProvider = new MetadataCollector()) {
 | |
|     this.resolveModuleNameHost = Object.create(tsHost);
 | |
|     this.resolveModuleNameHost.fileExists = (fileName) => {
 | |
|       fileName = stripNgResourceSuffix(fileName);
 | |
|       return tsHost.fileExists(fileName);
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   hideMetadata() { this.metadataVisible = false; }
 | |
| 
 | |
|   tsFilesOnly() { this.dtsAreSource = false; }
 | |
| 
 | |
|   // StaticSymbolResolverHost
 | |
|   getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined {
 | |
|     if (!this.tsHost.fileExists(modulePath)) {
 | |
|       return undefined;
 | |
|     }
 | |
|     if (DTS.test(modulePath)) {
 | |
|       if (this.metadataVisible) {
 | |
|         const metadataPath = modulePath.replace(DTS, '.metadata.json');
 | |
|         if (this.tsHost.fileExists(metadataPath)) {
 | |
|           let result = JSON.parse(this.tsHost.readFile(metadataPath));
 | |
|           return Array.isArray(result) ? result : [result];
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       const sf = this.tsHost.getSourceFile(modulePath, ts.ScriptTarget.Latest);
 | |
|       const metadata = this.metadataProvider.getMetadata(sf);
 | |
|       return metadata ? [metadata] : [];
 | |
|     }
 | |
|     return undefined;
 | |
|   }
 | |
| 
 | |
|   moduleNameToFileName(moduleName: string, containingFile: string): string|null {
 | |
|     if (!containingFile || !containingFile.length) {
 | |
|       if (moduleName.indexOf('.') === 0) {
 | |
|         throw new Error('Resolution of relative paths requires a containing file.');
 | |
|       }
 | |
|       // Any containing file gives the same result for absolute imports
 | |
|       containingFile = path.join('/', 'index.ts');
 | |
|     }
 | |
|     moduleName = moduleName.replace(EXT, '');
 | |
|     const resolved = ts.resolveModuleName(
 | |
|                            moduleName, containingFile.replace(/\\/g, '/'),
 | |
|                            {baseDir: '/', genDir: '/'}, this.resolveModuleNameHost)
 | |
|                          .resolvedModule;
 | |
|     return resolved ? resolved.resolvedFileName : null;
 | |
|   }
 | |
| 
 | |
|   getOutputName(filePath: string) { return filePath; }
 | |
| 
 | |
|   resourceNameToFileName(resourceName: string, containingFile: string) {
 | |
|     // Note: we convert package paths into relative paths to be compatible with the the
 | |
|     // previous implementation of UrlResolver.
 | |
|     if (resourceName && resourceName.charAt(0) !== '.' && !path.isAbsolute(resourceName)) {
 | |
|       resourceName = `./${resourceName}`;
 | |
|     }
 | |
|     const filePathWithNgResource =
 | |
|         this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile);
 | |
|     return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null;
 | |
|   }
 | |
| 
 | |
|   // AotSummaryResolverHost
 | |
|   loadSummary(filePath: string): string|null { return this.tsHost.readFile(filePath); }
 | |
| 
 | |
|   isSourceFile(sourceFilePath: string): boolean {
 | |
|     return !GENERATED_FILES.test(sourceFilePath) &&
 | |
|         (this.dtsAreSource || !DTS.test(sourceFilePath));
 | |
|   }
 | |
| 
 | |
|   toSummaryFileName(filePath: string): string { return filePath.replace(EXT, '') + '.d.ts'; }
 | |
| 
 | |
|   fromSummaryFileName(filePath: string): string { return filePath; }
 | |
| 
 | |
|   // AotCompilerHost
 | |
|   fileNameToModuleName(importedFile: string, containingFile: string): string {
 | |
|     return importedFile.replace(EXT, '');
 | |
|   }
 | |
| 
 | |
|   loadResource(path: string): string {
 | |
|     if (this.tsHost.fileExists(path)) {
 | |
|       return this.tsHost.readFile(path);
 | |
|     } else {
 | |
|       throw new Error(`Resource ${path} not found.`);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| export class MockMetadataBundlerHost implements MetadataBundlerHost {
 | |
|   private collector = new MetadataCollector();
 | |
| 
 | |
|   constructor(private host: ts.CompilerHost) {}
 | |
| 
 | |
|   getMetadataFor(moduleName: string): ModuleMetadata|undefined {
 | |
|     const source = this.host.getSourceFile(moduleName + '.ts', ts.ScriptTarget.Latest);
 | |
|     return source && this.collector.getMetadata(source);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function find(fileName: string, data: MockFileOrDirectory | undefined): MockFileOrDirectory|
 | |
|     undefined {
 | |
|   if (!data) return undefined;
 | |
|   const names = fileName.split('/');
 | |
|   if (names.length && !names[0].length) names.shift();
 | |
|   let current: MockFileOrDirectory|undefined = data;
 | |
|   for (const name of names) {
 | |
|     if (typeof current !== 'object') {
 | |
|       return undefined;
 | |
|     }
 | |
|     current = current[name];
 | |
|   }
 | |
|   return current;
 | |
| }
 | |
| 
 | |
| function open(fileName: string, data: MockFileOrDirectory | undefined): string|undefined {
 | |
|   let result = find(fileName, data);
 | |
|   if (typeof result === 'string') {
 | |
|     return result;
 | |
|   }
 | |
|   return undefined;
 | |
| }
 | |
| 
 | |
| function directoryExists(dirname: string, data: MockFileOrDirectory | undefined): boolean {
 | |
|   let result = find(dirname, data);
 | |
|   return !!result && typeof result !== 'string';
 | |
| }
 | |
| 
 | |
| export type MockFileArray = {
 | |
|   fileName: string,
 | |
|   content: string
 | |
| }[];
 | |
| 
 | |
| export type MockData = MockDirectory | Map<string, string>| (MockDirectory | Map<string, string>)[];
 | |
| 
 | |
| export function toMockFileArray(data: MockData, target: MockFileArray = []): MockFileArray {
 | |
|   if (data instanceof Map) {
 | |
|     mapToMockFileArray(data, target);
 | |
|   } else if (Array.isArray(data)) {
 | |
|     data.forEach(entry => toMockFileArray(entry, target));
 | |
|   } else {
 | |
|     mockDirToFileArray(data, '', target);
 | |
|   }
 | |
|   return target;
 | |
| }
 | |
| 
 | |
| function mockDirToFileArray(dir: MockDirectory, path: string, target: MockFileArray) {
 | |
|   Object.keys(dir).forEach((localFileName) => {
 | |
|     const value = dir[localFileName] !;
 | |
|     const fileName = `${path}/${localFileName}`;
 | |
|     if (typeof value === 'string') {
 | |
|       target.push({fileName, content: value});
 | |
|     } else {
 | |
|       mockDirToFileArray(value, fileName, target);
 | |
|     }
 | |
|   });
 | |
| }
 | |
| 
 | |
| function mapToMockFileArray(files: Map<string, string>, target: MockFileArray) {
 | |
|   files.forEach((content, fileName) => { target.push({fileName, content}); });
 | |
| }
 | |
| 
 | |
| export function arrayToMockMap(arr: MockFileArray): Map<string, string> {
 | |
|   const map = new Map<string, string>();
 | |
|   arr.forEach(({fileName, content}) => { map.set(fileName, content); });
 | |
|   return map;
 | |
| }
 | |
| 
 | |
| export function arrayToMockDir(arr: MockFileArray): MockDirectory {
 | |
|   const rootDir: MockDirectory = {};
 | |
|   arr.forEach(({fileName, content}) => {
 | |
|     let pathParts = fileName.split('/');
 | |
|     // trim trailing slash
 | |
|     let startIndex = pathParts[0] ? 0 : 1;
 | |
|     // get/create the directory
 | |
|     let currentDir = rootDir;
 | |
|     for (let i = startIndex; i < pathParts.length - 1; i++) {
 | |
|       const pathPart = pathParts[i];
 | |
|       let localDir = <MockDirectory>currentDir[pathPart];
 | |
|       if (!localDir) {
 | |
|         currentDir[pathPart] = localDir = {};
 | |
|       }
 | |
|       currentDir = localDir;
 | |
|     }
 | |
|     // write the file
 | |
|     currentDir[pathParts[pathParts.length - 1]] = content;
 | |
|   });
 | |
|   return rootDir;
 | |
| }
 | |
| 
 | |
| const minCoreIndex = `
 | |
|   export * from './src/application_module';
 | |
|   export * from './src/change_detection';
 | |
|   export * from './src/metadata';
 | |
|   export * from './src/di/metadata';
 | |
|   export * from './src/di/injectable';
 | |
|   export * from './src/di/injector';
 | |
|   export * from './src/di/injection_token';
 | |
|   export * from './src/linker';
 | |
|   export * from './src/render';
 | |
|   export * from './src/codegen_private_exports';
 | |
| `;
 | |
| 
 | |
| function readBazelWrittenFilesFrom(
 | |
|     bazelPackageRoot: string, packageName: string, map: Map<string, string>,
 | |
|     skip: (name: string, fullName: string) => boolean = () => false) {
 | |
|   function processDirectory(dir: string, dest: string) {
 | |
|     const entries = fs.readdirSync(dir);
 | |
|     for (const name of entries) {
 | |
|       const fullName = path.posix.join(dir, name);
 | |
|       const destName = path.posix.join(dest, name);
 | |
|       const stat = fs.statSync(fullName);
 | |
|       if (!skip(name, fullName)) {
 | |
|         if (stat.isDirectory()) {
 | |
|           processDirectory(fullName, destName);
 | |
|         } else {
 | |
|           const content = fs.readFileSync(fullName, 'utf8');
 | |
|           map.set(destName, content);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   try {
 | |
|     processDirectory(bazelPackageRoot, path.posix.join('/node_modules/@angular', packageName));
 | |
|     // todo: check why we always need an index.d.ts
 | |
|     if (fs.existsSync(path.join(bazelPackageRoot, `${packageName}.d.ts`))) {
 | |
|       const content = fs.readFileSync(path.join(bazelPackageRoot, `${packageName}.d.ts`), 'utf8');
 | |
|       map.set(path.posix.join('/node_modules/@angular', packageName, 'index.d.ts'), content);
 | |
|     }
 | |
|   } catch (e) {
 | |
|     console.error(
 | |
|         `Consider adding //packages/${packageName} as a data dependency in the BUILD.bazel rule for the failing test`);
 | |
|     throw e;
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function isInBazel(): boolean {
 | |
|   return process.env.TEST_SRCDIR != null;
 | |
| }
 | |
| 
 | |
| export function setup(options: {
 | |
|   compileAngular: boolean,
 | |
|   compileFakeCore?: boolean,
 | |
|   compileAnimations: boolean, compileCommon?: boolean
 | |
| } = {
 | |
|   compileAngular: true,
 | |
|   compileAnimations: true,
 | |
|   compileCommon: false,
 | |
|   compileFakeCore: false,
 | |
| }) {
 | |
|   let angularFiles = new Map<string, string>();
 | |
| 
 | |
|   beforeAll(() => {
 | |
|     const sources = process.env.TEST_SRCDIR;
 | |
|     if (sources) {
 | |
|       // If running under bazel then we get the compiled version of the files from the bazel package
 | |
|       // output.
 | |
|       const bundles = new Set([
 | |
|         'bundles', 'esm2015', 'esm5', 'testing', 'testing.d.ts', 'testing.metadata.json', 'browser',
 | |
|         'browser.d.ts'
 | |
|       ]);
 | |
|       const skipDirs = (name: string) => bundles.has(name);
 | |
|       if (options.compileAngular) {
 | |
|         // If this fails please add //packages/core:npm_package as a test data dependency.
 | |
|         readBazelWrittenFilesFrom(
 | |
|             resolveNpmTreeArtifact('angular/packages/core/npm_package'), 'core', angularFiles,
 | |
|             skipDirs);
 | |
|       }
 | |
|       if (options.compileFakeCore) {
 | |
|         readBazelWrittenFilesFrom(
 | |
|             resolveNpmTreeArtifact(
 | |
|                 'angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package'),
 | |
|             'core', angularFiles, skipDirs);
 | |
|       }
 | |
|       if (options.compileAnimations) {
 | |
|         // If this fails please add //packages/animations:npm_package as a test data dependency.
 | |
|         readBazelWrittenFilesFrom(
 | |
|             resolveNpmTreeArtifact('angular/packages/animations/npm_package'), 'animations',
 | |
|             angularFiles, skipDirs);
 | |
|       }
 | |
|       if (options.compileCommon) {
 | |
|         // If this fails please add //packages/common:npm_package as a test data dependency.
 | |
|         readBazelWrittenFilesFrom(
 | |
|             resolveNpmTreeArtifact('angular/packages/common/npm_package'), 'common', angularFiles,
 | |
|             skipDirs);
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (options.compileAngular) {
 | |
|       const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
 | |
|       emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
 | |
|       const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
 | |
|       emittingProgram.emit();
 | |
|       emittingHost.writtenAngularFiles(angularFiles);
 | |
|     }
 | |
|     if (options.compileCommon) {
 | |
|       const emittingHost =
 | |
|           new EmittingCompilerHost(['@angular/common/index.ts'], {emitMetadata: true});
 | |
|       const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
 | |
|       emittingProgram.emit();
 | |
|       emittingHost.writtenAngularFiles(angularFiles);
 | |
|     }
 | |
|     if (options.compileAnimations) {
 | |
|       const emittingHost =
 | |
|           new EmittingCompilerHost(['@angular/animations/index.ts'], {emitMetadata: true});
 | |
|       const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
 | |
|       emittingProgram.emit();
 | |
|       emittingHost.writtenAngularFiles(angularFiles);
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   return angularFiles;
 | |
| }
 | |
| 
 | |
| export function expectNoDiagnostics(program: ts.Program) {
 | |
|   function fileInfo(diagnostic: ts.Diagnostic): string {
 | |
|     if (diagnostic.file) {
 | |
|       return `${diagnostic.file.fileName}(${diagnostic.start}): `;
 | |
|     }
 | |
|     return '';
 | |
|   }
 | |
| 
 | |
|   function chars(len: number, ch: string): string { return newArray(len, ch).join(''); }
 | |
| 
 | |
|   function lineNoOf(offset: number, text: string): number {
 | |
|     let result = 1;
 | |
|     for (let i = 0; i < offset; i++) {
 | |
|       if (text[i] == '\n') result++;
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| 
 | |
|   function lineInfo(diagnostic: ts.Diagnostic): string {
 | |
|     if (diagnostic.file) {
 | |
|       const start = diagnostic.start !;
 | |
|       let end = diagnostic.start ! + diagnostic.length !;
 | |
|       const source = diagnostic.file.text;
 | |
|       let lineStart = start;
 | |
|       let lineEnd = end;
 | |
|       while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
 | |
|       if (lineStart < start) lineStart++;
 | |
|       while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
 | |
|       let line = source.substring(lineStart, lineEnd);
 | |
|       const lineIndex = line.indexOf('/n');
 | |
|       if (lineIndex > 0) {
 | |
|         line = line.substr(0, lineIndex);
 | |
|         end = start + lineIndex;
 | |
|       }
 | |
|       const lineNo = lineNoOf(start, source) + ': ';
 | |
|       return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
 | |
|           chars(end - start, '^');
 | |
|     }
 | |
|     return '';
 | |
|   }
 | |
| 
 | |
|   function expectNoDiagnostics(diagnostics: ReadonlyArray<ts.Diagnostic>) {
 | |
|     if (diagnostics && diagnostics.length) {
 | |
|       throw new Error(
 | |
|           'Errors from TypeScript:\n' +
 | |
|           diagnostics
 | |
|               .map(
 | |
|                   d =>
 | |
|                       `${fileInfo(d)}${ts.flattenDiagnosticMessageText(d.messageText, '\n')}${lineInfo(d)}`)
 | |
|               .join(' \n'));
 | |
|     }
 | |
|   }
 | |
|   expectNoDiagnostics(program.getOptionsDiagnostics());
 | |
|   expectNoDiagnostics(program.getSyntacticDiagnostics());
 | |
|   expectNoDiagnostics(program.getSemanticDiagnostics());
 | |
| }
 | |
| 
 | |
| export function isSource(fileName: string): boolean {
 | |
|   return !isDts(fileName) && /\.ts$/.test(fileName);
 | |
| }
 | |
| 
 | |
| function isDts(fileName: string): boolean {
 | |
|   return /\.d.ts$/.test(fileName);
 | |
| }
 | |
| 
 | |
| function isSourceOrDts(fileName: string): boolean {
 | |
|   return /\.ts$/.test(fileName) && !/(ngfactory|ngstyle|ngsummary).d.ts$/.test(fileName);
 | |
| }
 | |
| 
 | |
| function resolveNpmTreeArtifact(manifestPath: string, resolveFile = 'package.json') {
 | |
|   return path.dirname(require.resolve(path.posix.join(manifestPath, resolveFile)));
 | |
| }
 | |
| 
 | |
| export function compile(
 | |
|     rootDirs: MockData, options: {
 | |
|       emit?: boolean,
 | |
|       useSummaries?: boolean,
 | |
|       preCompile?: (program: ts.Program) => void,
 | |
|       postCompile?: (program: ts.Program) => void,
 | |
|     }& AotCompilerOptions = {},
 | |
|     tsOptions: ts.CompilerOptions = {}): {genFiles: GeneratedFile[], outDir: MockDirectory} {
 | |
|   // when using summaries, always emit so the next step can use the results.
 | |
|   const emit = options.emit || options.useSummaries;
 | |
|   const preCompile = options.preCompile || (() => {});
 | |
|   const postCompile = options.postCompile || expectNoDiagnostics;
 | |
|   const rootDirArr = toMockFileArray(rootDirs);
 | |
|   const scriptNames = rootDirArr.map(entry => entry.fileName)
 | |
|                           .filter(options.useSummaries ? isSource : isSourceOrDts);
 | |
| 
 | |
|   const host = new MockCompilerHost(scriptNames, arrayToMockDir(rootDirArr));
 | |
|   const aotHost = new MockAotCompilerHost(host);
 | |
|   if (options.useSummaries) {
 | |
|     aotHost.hideMetadata();
 | |
|     aotHost.tsFilesOnly();
 | |
|   }
 | |
|   const tsSettings = {...settings, ...tsOptions};
 | |
|   const program = ts.createProgram([...host.scriptNames], tsSettings, host);
 | |
|   preCompile(program);
 | |
|   const {compiler, reflector} = createAotCompiler(aotHost, options, (err) => { throw err; });
 | |
|   const analyzedModules =
 | |
|       compiler.analyzeModulesSync(program.getSourceFiles().map(sf => sf.fileName));
 | |
|   const genFiles = compiler.emitAllImpls(analyzedModules);
 | |
|   genFiles.forEach((file) => {
 | |
|     const source = file.source || toTypeScript(file);
 | |
|     if (isSource(file.genFileUrl)) {
 | |
|       host.addScript(file.genFileUrl, source);
 | |
|     } else {
 | |
|       host.override(file.genFileUrl, source);
 | |
|     }
 | |
|   });
 | |
|   const newProgram = ts.createProgram([...host.scriptNames], tsSettings, host);
 | |
|   postCompile(newProgram);
 | |
|   if (emit) {
 | |
|     newProgram.emit();
 | |
|   }
 | |
|   let outDir: MockDirectory = {};
 | |
|   if (emit) {
 | |
|     const dtsFilesWithGenFiles = new Set<string>(genFiles.map(gf => gf.srcFileUrl).filter(isDts));
 | |
|     outDir =
 | |
|         arrayToMockDir(toMockFileArray([host.writtenFiles, host.overrides])
 | |
|                            .filter((entry) => !isSource(entry.fileName))
 | |
|                            .concat(rootDirArr.filter(e => dtsFilesWithGenFiles.has(e.fileName))));
 | |
|   }
 | |
|   return {genFiles, outDir};
 | |
| }
 | |
| 
 | |
| function stripNgResourceSuffix(fileName: string): string {
 | |
|   return fileName.replace(/\.\$ngresource\$.*/, '');
 | |
| }
 | |
| 
 | |
| function addNgResourceSuffix(fileName: string): string {
 | |
|   return `${fileName}.$ngresource$`;
 | |
| }
 | |
| 
 | |
| function extractFileNames(directory: MockDirectory): string[] {
 | |
|   const result: string[] = [];
 | |
|   const scan = (directory: MockDirectory, prefix: string) => {
 | |
|     for (let name of Object.getOwnPropertyNames(directory)) {
 | |
|       const entry = directory[name];
 | |
|       const fileName = `${prefix}/${name}`;
 | |
|       if (typeof entry === 'string') {
 | |
|         result.push(fileName);
 | |
|       } else if (entry) {
 | |
|         scan(entry, fileName);
 | |
|       }
 | |
|     }
 | |
|   };
 | |
|   scan(directory, '');
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| export function emitLibrary(
 | |
|     context: Map<string, string>, mockData: MockDirectory,
 | |
|     scriptFiles?: string[]): Map<string, string> {
 | |
|   const emittingHost = new EmittingCompilerHost(
 | |
|       scriptFiles || extractFileNames(mockData), {emitMetadata: true, mockData, context});
 | |
|   const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
 | |
|   expectNoDiagnostics(emittingProgram);
 | |
|   emittingProgram.emit();
 | |
|   return emittingHost.written;
 | |
| }
 | |
| 
 | |
| export function mergeMaps<K, V>(...maps: Map<K, V>[]): Map<K, V> {
 | |
|   const result = new Map<K, V>();
 | |
| 
 | |
|   for (const map of maps) {
 | |
|     for (const [key, value] of Array.from(map.entries())) {
 | |
|       result.set(key, value);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return result;
 | |
| }
 |