/** * @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 * 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 NODE_MODULES = '/node_modules/'; const IS_GENERATED = /\.(ngfactory|ngstyle)$/; const angularts = /@angular\/(\w|\/|-)+\.tsx?$/; const rxjs = /\/rxjs\//; const tsxfile = /\.tsx$/; 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; } 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(); private writtenFiles = new Map(); private scriptNames: string[]; private root = '/'; private collector = new MetadataCollector(); private cachedAddedDirectories: Set|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()): Map { 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) { 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 { 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) => { 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 { let result = this.cachedAddedDirectories; if (!result) { const newCache = new Set(); 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(); public writtenFiles = new Map(); private sourceFiles = new Map(); private assumeExists = new Set(); private traces: string[] = []; constructor(scriptNames: string[], private data: MockDirectory) { this.scriptNames = scriptNames.slice(0); } // 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| (MockDirectory | Map)[]; 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, target: MockFileArray) { files.forEach((content, fileName) => { target.push({fileName, content}); }); } export function arrayToMockMap(arr: MockFileArray): Map { const map = new Map(); 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 = 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, 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.join(dir, name); const destName = path.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.join('/node_modules/@angular', packageName)); } 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, compileAnimations: boolean, compileCommon?: boolean} = { compileAngular: true, compileAnimations: true, compileCommon: false, }) { let angularFiles = new Map(); 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( path.join(sources, 'angular/packages/core/npm_package'), 'core', angularFiles, skipDirs); } if (options.compileAnimations) { // If this fails please add //packages/animations:npm_package as a test data dependency. readBazelWrittenFilesFrom( path.join(sources, '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( path.join(sources, '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 new Array(len).fill(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) { 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); } 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.slice(0), 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.slice(0), tsSettings, host); postCompile(newProgram); if (emit) { newProgram.emit(); } let outDir: MockDirectory = {}; if (emit) { const dtsFilesWithGenFiles = new Set(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, mockData: MockDirectory, scriptFiles?: string[]): Map { 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(...maps: Map[]): Map { const result = new Map(); for (const map of maps) { for (const [key, value] of Array.from(map.entries())) { result.set(key, value); } } return result; }