feat(compiler-cli): new compiler api and command-line using TypeScript transformers

This commit is contained in:
Chuck Jazdzewski
2017-06-09 14:50:57 -07:00
committed by Matias Niemelä
parent 43c187b624
commit 3097083277
31 changed files with 1990 additions and 115 deletions

View File

@ -21,7 +21,7 @@ const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsum
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
export interface CompilerHostContext extends ts.ModuleResolutionHost {
readResource(fileName: string): Promise<string>;
readResource?(fileName: string): Promise<string>|string;
assumeFileExists(fileName: string): void;
}
@ -187,10 +187,11 @@ export class CompilerHost implements AotCompilerHost {
getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
if (!this.context.fileExists(filePath)) {
// If the file doesn't exists then we cannot return metadata for the file.
// This will occur if the user refernced a declared module for which no file
// This will occur if the user referenced a declared module for which no file
// exists for the module (i.e. jQuery or angularjs).
return;
}
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (this.context.fileExists(metadataPath)) {
@ -202,11 +203,11 @@ export class CompilerHost implements AotCompilerHost {
return [this.upgradeVersion1Metadata(
{'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)];
}
} else {
const sf = this.getSourceFile(filePath);
const metadata = this.metadataCollector.getMetadata(sf);
return metadata ? [metadata] : [];
}
const sf = this.getSourceFile(filePath);
const metadata = this.metadataCollector.getMetadata(sf);
return metadata ? [metadata] : [];
}
readMetadata(filePath: string, dtsFilePath: string): ModuleMetadata[] {
@ -259,7 +260,8 @@ export class CompilerHost implements AotCompilerHost {
}
loadResource(filePath: string): Promise<string>|string {
return this.context.readResource(filePath);
if (this.context.readResource) return this.context.readResource(filePath);
return this.context.readFile(filePath);
}
loadSummary(filePath: string): string|null {

View File

@ -9,6 +9,8 @@
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, NgAnalyzedModules, ParseSourceSpan, Statement, StaticReflector, TypeScriptEmitter, createAotCompiler} from '@angular/compiler';
import * as ts from 'typescript';
import {Diagnostic, DiagnosticCategory} from '../transformers/api';
interface FactoryInfo {
source: ts.SourceFile;
context: EmitterVisitorContext;
@ -16,17 +18,10 @@ interface FactoryInfo {
type FactoryInfoMap = Map<string, FactoryInfo>;
export enum DiagnosticKind {
Message,
Warning,
Error,
}
export interface Diagnostic {
message: string;
kind: DiagnosticKind;
span: ParseSourceSpan;
}
const stubCancellationToken: ts.CancellationToken = {
isCancellationRequested(): boolean{return false;},
throwIfCancellationRequested(): void{}
};
export class TypeChecker {
private _aotCompiler: AotCompiler|undefined;
@ -35,6 +30,8 @@ export class TypeChecker {
private _factoryNames: string[]|undefined;
private _diagnosticProgram: ts.Program|undefined;
private _diagnosticsByFile: Map<string, Diagnostic[]>|undefined;
private _currentCancellationToken: ts.CancellationToken = stubCancellationToken;
private _partial: boolean = false;
constructor(
private program: ts.Program, private tsOptions: ts.CompilerOptions,
@ -42,12 +39,19 @@ export class TypeChecker {
private aotOptions: AotCompilerOptions, private _analyzedModules?: NgAnalyzedModules,
private _generatedFiles?: GeneratedFile[]) {}
getDiagnostics(fileName?: string): Diagnostic[] {
return fileName ?
this.diagnosticsByFileName.get(fileName) || [] :
([] as Diagnostic[]).concat(...Array.from(this.diagnosticsByFileName.values()));
getDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): Diagnostic[] {
this._currentCancellationToken = cancellationToken || stubCancellationToken;
try {
return fileName ?
this.diagnosticsByFileName.get(fileName) || [] :
([] as Diagnostic[]).concat(...Array.from(this.diagnosticsByFileName.values()));
} finally {
this._currentCancellationToken = stubCancellationToken;
}
}
get partialResults(): boolean { return this._partial; }
private get analyzedModules(): NgAnalyzedModules {
return this._analyzedModules || (this._analyzedModules = this.aotCompiler.analyzeModulesSync(
this.program.getSourceFiles().map(sf => sf.fileName)));
@ -130,6 +134,7 @@ export class TypeChecker {
};
const program = this.diagnosticProgram;
for (const factoryName of this.factoryNames) {
if (this._currentCancellationToken.isCancellationRequested()) return result;
const sourceFile = program.getSourceFile(factoryName);
for (const diagnostic of this.diagnosticProgram.getSemanticDiagnostics(sourceFile)) {
const span = this.sourceSpanOf(diagnostic.file, diagnostic.start, diagnostic.length);
@ -138,7 +143,7 @@ export class TypeChecker {
const diagnosticsList = diagnosticsFor(fileName);
diagnosticsList.push({
message: diagnosticMessageToString(diagnostic.messageText),
kind: diagnosticKindConverter(diagnostic.category), span
category: diagnosticCategoryConverter(diagnostic.category), span
});
}
}
@ -158,12 +163,12 @@ export class TypeChecker {
}
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {
return typeof message === 'string' ? message : diagnosticMessageToString(message.messageText);
return ts.flattenDiagnosticMessageText(message, '\n');
}
function diagnosticKindConverter(kind: ts.DiagnosticCategory) {
function diagnosticCategoryConverter(kind: ts.DiagnosticCategory) {
// The diagnostics kind matches ts.DiagnosticCategory. Review this code if this changes.
return kind as any as DiagnosticKind;
return kind as any as DiagnosticCategory;
}
function createFactoryInfo(emitter: TypeScriptEmitter, file: GeneratedFile): FactoryInfo {

View File

@ -0,0 +1,182 @@
/**
* @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
*/
// Must be imported first, because Angular decorators throw on load.
import 'reflect-metadata';
import {isSyntaxError, syntaxError} from '@angular/compiler';
import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import * as api from './transformers/api';
import * as ng from './transformers/entry_points';
const TS_EXT = /\.ts$/;
type Diagnostics = ts.Diagnostic[] | api.Diagnostic[];
function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] {
return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText);
}
function formatDiagnostics(cwd: string, diags: Diagnostics): string {
if (diags && diags.length) {
if (isTsDiagnostics(diags)) {
return ts.formatDiagnostics(diags, {
getCurrentDirectory(): string{return cwd;},
getCanonicalFileName(fileName: string): string{return fileName;},
getNewLine(): string{return '\n';}
});
} else {
return diags
.map(d => {
let res = api.DiagnosticCategory[d.category];
if (d.span) {
res +=
` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`;
}
if (d.span && d.span.details) {
res += `: ${d.span.details}, ${d.message}\n`;
} else {
res += `: ${d.message}\n`;
}
return res;
})
.join();
}
} else
return '';
}
function check(cwd: string, ...args: Diagnostics[]) {
if (args.some(diags => !!(diags && diags[0]))) {
throw syntaxError(args.map(diags => {
if (diags && diags[0]) {
return formatDiagnostics(cwd, diags);
}
})
.filter(message => !!message)
.join(''));
}
}
function syntheticError(message: string): ts.Diagnostic {
return {
file: null as any as ts.SourceFile,
start: 0,
length: 0,
messageText: message,
category: ts.DiagnosticCategory.Error,
code: 0
};
}
export function readConfiguration(
project: string, basePath: string, existingOptions?: ts.CompilerOptions) {
// Allow a directory containing tsconfig.json as the project value
// Note, TS@next returns an empty array, while earlier versions throw
const projectFile =
fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project;
let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile);
if (error) check(basePath, [error]);
const parseConfigHost = {
useCaseSensitiveFileNames: true,
fileExists: fs.existsSync,
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile
};
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions);
check(basePath, parsed.errors);
// Default codegen goes to the current directory
// Parsed options are already converted to absolute paths
const ngOptions = config.angularCompilerOptions || {};
// Ignore the genDir option
ngOptions.genDir = basePath;
for (const key of Object.keys(parsed.options)) {
ngOptions[key] = parsed.options[key];
}
return {parsed, ngOptions};
}
export function main(args: string[], consoleError: (s: string) => void = console.error): number {
try {
const parsedArgs = require('minimist')(args);
const project = parsedArgs.p || parsedArgs.project || '.';
const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project;
// file names in tsconfig are resolved relative to this absolute path
const basePath = path.resolve(process.cwd(), projectDir);
const {parsed, ngOptions} = readConfiguration(project, basePath);
ngOptions.basePath = basePath;
let host = ts.createCompilerHost(parsed.options, true);
const rootFileNames = parsed.fileNames.slice(0);
const addGeneratedFileName =
(fileName: string) => {
if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) {
rootFileNames.push(fileName);
}
}
if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) {
const {host: bundleHost, indexName, errors} =
createBundleIndexHost(ngOptions, rootFileNames, host);
if (errors) check(basePath, errors);
if (indexName) addGeneratedFileName(indexName);
host = bundleHost;
}
const ngHost = ng.createHost({tsHost: host, options: ngOptions});
const ngProgram =
ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngOptions});
// Check parameter diagnostics
check(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics());
// Check syntactic diagnostics
check(basePath, ngProgram.getTsSyntacticDiagnostics());
// Check TypeScript semantic and Angular structure diagnostics
check(basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics());
// Check Angular semantic diagnostics
check(basePath, ngProgram.getNgSemanticDiagnostics());
ngProgram.emit({
emitFlags: api.EmitFlags.Default |
((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata)
});
} catch (e) {
if (isSyntaxError(e)) {
consoleError(e.message);
return 1;
} else {
consoleError(e.stack);
consoleError('Compilation failed');
return 2;
}
}
return 0;
}
// CLI entry point
if (require.main === module) {
process.exit(main(process.argv.slice(2), s => console.error(s)));
}

View File

@ -79,7 +79,6 @@ class CustomLoaderModuleResolutionHostAdapter extends ModuleResolutionHostAdapte
readResource(path: string) { return this._readResource(path); }
}
/**
* @internal
* @private
@ -92,6 +91,7 @@ export class NgTools_InternalApi_NG_2 {
static codeGen(options: NgTools_InternalApi_NG2_CodeGen_Options): Promise<any> {
const hostContext: CompilerHostContext =
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
const cliOptions: NgcCliOptions = {
i18nFormat: options.i18nFormat !,
i18nFile: options.i18nFile !,

View File

@ -0,0 +1,227 @@
/**
* @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 {ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript';
export enum DiagnosticCategory {
Warning = 0,
Error = 1,
Message = 2,
}
export interface Diagnostic {
message: string;
span?: ParseSourceSpan;
category: DiagnosticCategory;
}
export interface CompilerOptions extends ts.CompilerOptions {
// Absolute path to a directory where generated file structure is written.
// If unspecified, generated files will be written alongside sources.
genDir?: string;
// Path to the directory containing the tsconfig.json file.
basePath?: string;
// Don't produce .metadata.json files (they don't work for bundled emit with --out)
skipMetadataEmit?: boolean;
// Produce an error if the metadata written for a class would produce an error if used.
strictMetadataEmit?: boolean;
// Don't produce .ngfactory.ts or .ngstyle.ts files
skipTemplateCodegen?: boolean;
// Whether to generate a flat module index of the given name and the corresponding
// flat module metadata. This option is intended to be used when creating flat
// modules similar to how `@angular/core` and `@angular/common` are packaged.
// When this option is used the `package.json` for the library should refered to the
// generated flat module index instead of the library index file. When using this
// option only one .metadata.json file is produced that contains all the metadata
// necessary for symbols exported from the library index.
// In the generated .ngfactory.ts files flat module index is used to import symbols
// includes both the public API from the library index as well as shrowded internal
// symbols.
// By default the .ts file supplied in the `files` files field is assumed to be
// library index. If more than one is specified, uses `libraryIndex` to select the
// file to use. If more than on .ts file is supplied and no `libraryIndex` is supplied
// an error is produced.
// A flat module index .d.ts and .js will be created with the given `flatModuleOutFile`
// name in the same location as the library index .d.ts file is emitted.
// For example, if a library uses `public_api.ts` file as the library index of the
// module the `tsconfig.json` `files` field would be `["public_api.ts"]`. The
// `flatModuleOutFile` options could then be set to, for example `"index.js"`, which
// produces `index.d.ts` and `index.metadata.json` files. The library's
// `package.json`'s `module` field would be `"index.js"` and the `typings` field would
// be `"index.d.ts"`.
flatModuleOutFile?: string;
// Preferred module id to use for importing flat module. References generated by `ngc`
// will use this module name when importing symbols from the flat module. This is only
// meaningful when `flatModuleOutFile` is also supplied. It is otherwise ignored.
flatModuleId?: string;
// Whether to generate code for library code.
// If true, produce .ngfactory.ts and .ngstyle.ts files for .d.ts inputs.
// Default is true.
generateCodeForLibraries?: boolean;
// Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean;
// Modify how angular annotations are emitted to improve tree-shaking.
// Default is static fields.
// decorators: Leave the Decorators in-place. This makes compilation faster.
// TypeScript will emit calls to the __decorate helper.
// `--emitDecoratorMetadata` can be used for runtime reflection.
// However, the resulting code will not properly tree-shake.
// static fields: Replace decorators with a static field in the class.
// Allows advanced tree-shakers like Closure Compiler to remove
// unused classes.
annotationsAs?: 'decorators'|'static fields';
// Print extra information while running the compiler
trace?: boolean;
// Whether to enable support for <template> and the template attribute (true by default)
enableLegacyTemplate?: boolean;
}
export interface ModuleFilenameResolver {
/**
* Converts a module name that is used in an `import` to a file path.
* I.e. `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
*/
moduleNameToFileName(moduleName: string, containingFile?: string): string|null;
/**
* Converts a file path to a module name that can be used as an `import.
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
*
* See ImportResolver.
*/
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
getNgCanonicalFileName(fileName: string): string;
assumeFileExists(fileName: string): void;
}
export interface CompilerHost extends ts.CompilerHost, ModuleFilenameResolver {
/**
* Load a referenced resource either statically or asynchronously. If the host returns a
* `Promise<string>` it is assumed the user of the corresponding `Program` will call
* `loadNgStructureAsync()`. Returing `Promise<string>` outside `loadNgStructureAsync()` will
* cause a diagnostics diagnostic error or an exception to be thrown.
*
* If `loadResource()` is not provided, `readFile()` will be called to load the resource.
*/
readResource?(fileName: string): Promise<string>|string;
}
export enum EmitFlags {
DTS = 1 << 0,
JS = 1 << 1,
Metadata = 1 << 2,
I18nBundle = 1 << 3,
Summary = 1 << 4,
Default = DTS | JS,
All = DTS | JS | Metadata | I18nBundle | Summary
}
// TODO(chuckj): Support CustomTransformers once we require TypeScript 2.3+
// export interface CustomTransformers {
// beforeTs?: ts.TransformerFactory<ts.SourceFile>[];
// afterTs?: ts.TransformerFactory<ts.SourceFile>[];
// }
export interface Program {
/**
* Retrieve the TypeScript program used to produce semantic diagnostics and emit the sources.
*
* Angular structural information is required to produce the program.
*/
getTsProgram(): ts.Program;
/**
* Retreive options diagnostics for the TypeScript options used to create the program. This is
* faster than calling `getTsProgram().getOptionsDiagnostics()` since it does not need to
* collect Angular structural information to produce the errors.
*/
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken): ts.Diagnostic[];
/**
* Retrieve options diagnostics for the Angular options used to create the program.
*/
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[];
/**
* Retrive the syntax diagnostics from TypeScript. This is faster than calling
* `getTsProgram().getSyntacticDiagnostics()` since it does not need to collect Angular structural
* information to produce the errors.
*/
getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ts.Diagnostic[];
/**
* Retrieve the diagnostics for the structure of an Angular application is correctly formed.
* This includes validating Angular annotations and the syntax of referenced and imbedded HTML
* and CSS.
*
* Note it is important to displaying TypeScript semantic diagnostics along with Angular
* structural diagnostics as an error in the program strucutre might cause errors detected in
* semantic analysis and a semantic error might cause errors in specifying the program structure.
*
* Angular structural information is required to produce these diagnostics.
*/
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[];
/**
* Retreive the semantic diagnostics from TypeScript. This is equivilent to calling
* `getTsProgram().getSemanticDiagnostics()` directly and is included for completeness.
*/
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ts.Diagnostic[];
/**
* Retrieve the Angular semantic diagnostics.
*
* Angular structural information is required to produce these diagnostics.
*/
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
Diagnostic[];
/**
* Load Angular structural information asynchronously. If this method is not called then the
* Angular structural information, including referenced HTML and CSS files, are loaded
* synchronously. If the supplied Angular compiler host returns a promise from `loadResource()`
* will produce a diagnostic error message or, `getTsProgram()` or `emit` to throw.
*/
loadNgStructureAsync(): Promise<void>;
/**
* Retrieve the lazy route references in the program.
*
* Angular structural information is required to produce these routes.
*/
getLazyRoutes(cancellationToken?: ts.CancellationToken): {[route: string]: string};
/**
* Emit the files requested by emitFlags implied by the program.
*
* Angular structural information is required to emit files.
*/
emit({// transformers,
emitFlags, cancellationToken}: {
emitFlags: EmitFlags,
// transformers?: CustomTransformers, // See TODO above
cancellationToken?: ts.CancellationToken,
}): void;
}

View File

@ -0,0 +1,24 @@
import * as ts from 'typescript';
import {CompilerHost, CompilerOptions, Program} from './api';
import {createModuleFilenameResolver} from './module_filename_resolver';
export {createProgram} from './program';
export {createModuleFilenameResolver};
export function createHost({tsHost, options}: {tsHost: ts.CompilerHost, options: CompilerOptions}):
CompilerHost {
const resolver = createModuleFilenameResolver(tsHost, options);
const host = Object.create(tsHost);
host.moduleNameToFileName = resolver.moduleNameToFileName.bind(resolver);
host.fileNameToModuleName = resolver.fileNameToModuleName.bind(resolver);
host.getNgCanonicalFileName = resolver.getNgCanonicalFileName.bind(resolver);
host.assumeFileExists = resolver.assumeFileExists.bind(resolver);
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
// https://github.com/Microsoft/TypeScript/issues/9552
host.realpath = (fileName: string) => fileName;
return host;
}

View File

@ -0,0 +1,285 @@
import * as path from 'path';
import * as ts from 'typescript';
import {ModuleFilenameResolver} from './api';
import {CompilerOptions} from './api';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
const NODE_MODULES = '/node_modules/';
const IS_GENERATED = /\.(ngfactory|ngstyle|ngsummary)$/;
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
export function createModuleFilenameResolver(
tsHost: ts.ModuleResolutionHost, options: CompilerOptions): ModuleFilenameResolver {
const host = createModuleFilenameResolverHost(tsHost);
return options.rootDirs && options.rootDirs.length > 0 ?
new MultipleRootDirModuleFilenameResolver(host, options) :
new SingleRootDirModuleFilenameResolver(host, options);
}
class SingleRootDirModuleFilenameResolver implements ModuleFilenameResolver {
private isGenDirChildOfRootDir: boolean;
private basePath: string;
private genDir: string;
private moduleFileNames = new Map<string, string|null>();
constructor(private host: ModuleFilenameResolutionHost, private options: CompilerOptions) {
// normalize the path so that it never ends with '/'.
this.basePath = path.normalize(path.join(options.basePath, '.')).replace(/\\/g, '/');
this.genDir = path.normalize(path.join(options.genDir, '.')).replace(/\\/g, '/');
const genPath: string = path.relative(this.basePath, this.genDir);
this.isGenDirChildOfRootDir = genPath === '' || !genPath.startsWith('..');
}
moduleNameToFileName(m: string, containingFile: string): string|null {
const key = m + ':' + (containingFile || '');
let result: string|null = this.moduleFileNames.get(key) || null;
if (!result) {
if (!containingFile) {
if (m.indexOf('.') === 0) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// Any containing file gives the same result for absolute imports
containingFile = this.getNgCanonicalFileName(path.join(this.basePath, 'index.ts'));
}
m = m.replace(EXT, '');
const resolved =
ts.resolveModuleName(m, containingFile.replace(/\\/g, '/'), this.options, this.host)
.resolvedModule;
result = resolved ? this.getNgCanonicalFileName(resolved.resolvedFileName) : null;
this.moduleFileNames.set(key, result);
}
return result;
}
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
*
* The `containingFile` is always in the `genDir`, where as the `importedFile` can be in
* `genDir`, `node_module` or `basePath`. The `importedFile` is either a generated file or
* existing file.
*
* | genDir | node_module | rootDir
* --------------+----------+-------------+----------
* generated | relative | relative | n/a
* existing file | n/a | absolute | relative(*)
*
* NOTE: (*) the relative path is computed depending on `isGenDirChildOfRootDir`.
*/
fileNameToModuleName(importedFile: string, containingFile: string): string {
// If a file does not yet exist (because we compile it later), we still need to
// assume it exists it so that the `resolve` method works!
if (!this.host.fileExists(importedFile)) {
this.host.assumeFileExists(importedFile);
}
containingFile = this.rewriteGenDirPath(containingFile);
const containingDir = path.dirname(containingFile);
// drop extension
importedFile = importedFile.replace(EXT, '');
const nodeModulesIndex = importedFile.indexOf(NODE_MODULES);
const importModule = nodeModulesIndex === -1 ?
null :
importedFile.substring(nodeModulesIndex + NODE_MODULES.length);
const isGeneratedFile = IS_GENERATED.test(importedFile);
if (isGeneratedFile) {
// rewrite to genDir path
if (importModule) {
// it is generated, therefore we do a relative path to the factory
return this.dotRelative(containingDir, this.genDir + NODE_MODULES + importModule);
} else {
// assume that import is also in `genDir`
importedFile = this.rewriteGenDirPath(importedFile);
return this.dotRelative(containingDir, importedFile);
}
} else {
// user code import
if (importModule) {
return importModule;
} else {
if (!this.isGenDirChildOfRootDir) {
// assume that they are on top of each other.
importedFile = importedFile.replace(this.basePath, this.genDir);
}
if (SHALLOW_IMPORT.test(importedFile)) {
return importedFile;
}
return this.dotRelative(containingDir, importedFile);
}
}
}
// We use absolute paths on disk as canonical.
getNgCanonicalFileName(fileName: string): string { return fileName; }
assumeFileExists(fileName: string) { this.host.assumeFileExists(fileName); }
private dotRelative(from: string, to: string): string {
const rPath: string = path.relative(from, to).replace(/\\/g, '/');
return rPath.startsWith('.') ? rPath : './' + rPath;
}
/**
* Moves the path into `genDir` folder while preserving the `node_modules` directory.
*/
private rewriteGenDirPath(filepath: string) {
const nodeModulesIndex = filepath.indexOf(NODE_MODULES);
if (nodeModulesIndex !== -1) {
// If we are in node_module, transplant them into `genDir`.
return path.join(this.genDir, filepath.substring(nodeModulesIndex));
} else {
// pretend that containing file is on top of the `genDir` to normalize the paths.
// we apply the `genDir` => `rootDir` delta through `rootDirPrefix` later.
return filepath.replace(this.basePath, this.genDir);
}
}
}
/**
* This version of the AotCompilerHost expects that the program will be compiled
* and executed with a "path mapped" directory structure, where generated files
* are in a parallel tree with the sources, and imported using a `./` relative
* import. This requires using TS `rootDirs` option and also teaching the module
* loader what to do.
*/
class MultipleRootDirModuleFilenameResolver implements ModuleFilenameResolver {
private basePath: string;
constructor(private host: ModuleFilenameResolutionHost, private options: CompilerOptions) {
// normalize the path so that it never ends with '/'.
this.basePath = path.normalize(path.join(options.basePath, '.')).replace(/\\/g, '/');
}
getNgCanonicalFileName(fileName: string): string {
if (!fileName) return fileName;
// NB: the rootDirs should have been sorted longest-first
for (const dir of this.options.rootDirs || []) {
if (fileName.indexOf(dir) === 0) {
fileName = fileName.substring(dir.length);
}
}
return fileName;
}
assumeFileExists(fileName: string) { this.host.assumeFileExists(fileName); }
moduleNameToFileName(m: string, containingFile: string): string|null {
if (!containingFile) {
if (m.indexOf('.') === 0) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// Any containing file gives the same result for absolute imports
containingFile = this.getNgCanonicalFileName(path.join(this.basePath, 'index.ts'));
}
for (const root of this.options.rootDirs || ['']) {
const rootedContainingFile = path.join(root, containingFile);
const resolved =
ts.resolveModuleName(m, rootedContainingFile, this.options, this.host).resolvedModule;
if (resolved) {
if (this.options.traceResolution) {
console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName);
}
return this.getNgCanonicalFileName(resolved.resolvedFileName);
}
}
return null;
}
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
* they are resolvable by the moduleResolution strategy from the CompilerHost.
*/
fileNameToModuleName(importedFile: string, containingFile: string): string {
if (this.options.traceResolution) {
console.error(
'getImportPath from containingFile', containingFile, 'to importedFile', importedFile);
}
// If a file does not yet exist (because we compile it later), we still need to
// assume it exists so that the `resolve` method works!
if (!this.host.fileExists(importedFile)) {
if (this.options.rootDirs && this.options.rootDirs.length > 0) {
this.host.assumeFileExists(path.join(this.options.rootDirs[0], importedFile));
} else {
this.host.assumeFileExists(importedFile);
}
}
const resolvable = (candidate: string) => {
const resolved = this.moduleNameToFileName(candidate, importedFile);
return resolved && resolved.replace(EXT, '') === importedFile.replace(EXT, '');
};
const importModuleName = importedFile.replace(EXT, '');
const parts = importModuleName.split(path.sep).filter(p => !!p);
let foundRelativeImport: string|undefined;
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (resolvable(candidate)) {
return candidate;
}
candidate = '.' + path.sep + candidate;
if (resolvable(candidate)) {
foundRelativeImport = candidate;
}
}
if (foundRelativeImport) return foundRelativeImport;
// Try a relative import
const candidate = path.relative(path.dirname(containingFile), importModuleName);
if (resolvable(candidate)) {
return candidate;
}
throw new Error(
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
}
interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost {
assumeFileExists(fileName: string): void;
}
function createModuleFilenameResolverHost(host: ts.ModuleResolutionHost):
ModuleFilenameResolutionHost {
const assumedExists = new Set<string>();
const resolveModuleNameHost = Object.create(host);
// When calling ts.resolveModuleName, additional allow checks for .d.ts files to be done based on
// checks for .ngsummary.json files, so that our codegen depends on fewer inputs and requires
// to be called less often.
// This is needed as we use ts.resolveModuleName in reflector_host and it should be able to
// resolve summary file names.
resolveModuleNameHost.fileExists = (fileName: string): boolean => {
if (assumedExists.has(fileName)) {
return true;
}
if (host.fileExists(fileName)) {
return true;
}
if (DTS.test(fileName)) {
const base = fileName.substring(0, fileName.length - 5);
return host.fileExists(base + '.ngsummary.json');
}
return false;
};
resolveModuleNameHost.assumeFileExists = (fileName: string) => assumedExists.add(fileName);
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
// https://github.com/Microsoft/TypeScript/issues/9552
resolveModuleNameHost.realpath = (fileName: string) => fileName;
return resolveModuleNameHost;
}

View File

@ -16,9 +16,8 @@ const CATCH_ERROR_NAME = 'error';
const CATCH_STACK_NAME = 'stack';
export class TypeScriptNodeEmitter {
updateSourceFile(
sourceFile: ts.SourceFile, srcFilePath: string, genFilePath: string, stmts: Statement[],
exportedVars: string[], preamble?: string): [ts.SourceFile, Map<ts.Node, Node>] {
updateSourceFile(sourceFile: ts.SourceFile, stmts: Statement[], preamble?: string):
[ts.SourceFile, Map<ts.Node, Node>] {
const converter = new _NodeEmitterVisitor();
const statements =
stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null);
@ -99,9 +98,6 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
if (stmt.hasModifier(StmtModifier.Exported)) {
modifiers.push(ts.createToken(ts.SyntaxKind.ExportKeyword));
}
if (stmt.hasModifier(StmtModifier.Final)) {
modifiers.push(ts.createToken(ts.SyntaxKind.ConstKeyword));
}
return modifiers;
}
@ -140,7 +136,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
p => ts.createParameter(
/* decorators */ undefined, /* modifiers */ undefined,
/* dotDotDotToken */ undefined, p.name)),
undefined, this._visitStatements(stmt.statements)));
/* type */ undefined, this._visitStatements(stmt.statements)));
}
visitExpressionStmt(stmt: ExpressionStatement) {
@ -187,7 +183,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
p => ts.createParameter(
/* decorators */ undefined, /* modifiers */ undefined,
/* dotDotDotToken */ undefined, p.name)),
undefined, this._visitStatements(method.body)));
/* type */ undefined, this._visitStatements(method.body)));
return this.record(
stmt, ts.createClassDeclaration(
/* decorators */ undefined, modifiers, stmt.name, /* typeParameters*/ undefined,
@ -221,7 +217,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
ts.createIdentifier(CATCH_ERROR_NAME),
ts.createIdentifier(CATCH_STACK_NAME)))])],
stmt.catchStmts)),
undefined));
/* finallyBlock */ undefined));
}
visitThrowStmt(stmt: ThrowStmt) {

View File

@ -0,0 +1,29 @@
/**
* @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 {GeneratedFile} from '@angular/compiler';
import * as ts from 'typescript';
import {TypeScriptNodeEmitter} from './node_emitter';
export function getAngularEmitterTransformFactory(generatedFiles: GeneratedFile[]): () =>
(sourceFile: ts.SourceFile) => ts.SourceFile {
return function() {
const map = new Map(generatedFiles.filter(g => g.stmts && g.stmts.length)
.map<[string, GeneratedFile]>(g => [g.genFileUrl, g]));
const emitter = new TypeScriptNodeEmitter();
return function(sourceFile: ts.SourceFile): ts.SourceFile {
const g = map.get(sourceFile.fileName);
if (g && g.stmts) {
const [newSourceFile] = emitter.updateSourceFile(sourceFile, g.stmts);
return newSourceFile;
}
return sourceFile;
};
};
}

View File

@ -0,0 +1,391 @@
/**
* @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 {AotCompiler, GeneratedFile, NgAnalyzedModules, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler';
import {MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
import {writeFileSync} from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {CompilerHost as AotCompilerHost, CompilerHostContext} from '../compiler_host';
import {TypeChecker} from '../diagnostics/check_types';
import {CompilerHost, CompilerOptions, DiagnosticCategory} from './api';
import {Diagnostic, EmitFlags, Program} from './api';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
const GENERATED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$|\.ngsummary\.js$/;
const SUMMARY_JSON_FILES = /\.ngsummary.json$/;
const emptyModules: NgAnalyzedModules = {
ngModules: [],
ngModuleByPipeOrDirective: new Map(),
files: []
};
class AngularCompilerProgram implements Program {
// Initialized in the constructor
private oldTsProgram: ts.Program|undefined;
private tsProgram: ts.Program;
private aotCompilerHost: AotCompilerHost;
private compiler: AotCompiler;
private srcNames: string[];
private collector: MetadataCollector;
// Lazily initialized fields
private _analyzedModules: NgAnalyzedModules|undefined;
private _structuralDiagnostics: Diagnostic[] = [];
private _stubs: GeneratedFile[]|undefined;
private _stubFiles: string[]|undefined;
private _programWithStubsHost: ts.CompilerHost|undefined;
private _programWithStubs: ts.Program|undefined;
private _generatedFiles: GeneratedFile[]|undefined;
private _generatedFileDiagnostics: Diagnostic[]|undefined;
private _typeChecker: TypeChecker|undefined;
private _semanticDiagnostics: Diagnostic[]|undefined;
constructor(
private rootNames: string[], private options: CompilerOptions, private host: CompilerHost,
private oldProgram?: Program) {
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram);
this.srcNames = this.tsProgram.getSourceFiles().map(sf => sf.fileName);
this.aotCompilerHost = new AotCompilerHost(this.tsProgram, options, host);
if (host.readResource) {
this.aotCompilerHost.loadResource = host.readResource.bind(host);
}
const {compiler} = createAotCompiler(this.aotCompilerHost, options);
this.compiler = compiler;
this.collector = new MetadataCollector({quotedNames: true});
}
// Program implementation
getTsProgram(): ts.Program { return this.programWithStubs; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
}
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] {
return getNgOptionDiagnostics(this.options);
}
getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ts.Diagnostic[] {
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
}
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] {
return this.structuralDiagnostics;
}
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ts.Diagnostic[] {
return this.programWithStubs.getSemanticDiagnostics(sourceFile, cancellationToken);
}
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
Diagnostic[] {
const compilerDiagnostics = this.generatedFileDiagnostics;
// If we have diagnostics during the parser phase the type check phase is not meaningful so skip
// it.
if (compilerDiagnostics && compilerDiagnostics.length) return compilerDiagnostics;
return this.typeChecker.getDiagnostics(fileName, cancellationToken);
}
loadNgStructureAsync(): Promise<void> {
return this.compiler.analyzeModulesAsync(this.rootNames)
.catch(this.catchAnalysisError.bind(this))
.then(analyzedModules => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronsly');
}
this._analyzedModules = analyzedModules;
});
}
getLazyRoutes(cancellationToken?: ts.CancellationToken): {[route: string]: string} { return {}; }
emit({emitFlags = EmitFlags.Default, cancellationToken}:
{emitFlags?: EmitFlags, cancellationToken?: ts.CancellationToken}): ts.EmitResult {
const emitMap = new Map<string, string>();
const result = this.programWithStubs.emit(
/* targetSourceFile */ undefined,
createWriteFileCallback(emitFlags, this.host, this.collector, this.options, emitMap),
cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, {
after: this.options.skipTemplateCodegen ? [] : [getAngularEmitterTransformFactory(
this.generatedFiles)]
});
this.generatedFiles.forEach(file => {
if (file.source && file.source.length && SUMMARY_JSON_FILES.test(file.genFileUrl)) {
// If we have emitted the ngsummary.ts file, ensure the ngsummary.json file is emitted to
// the same location.
const emittedFile = emitMap.get(file.srcFileUrl);
const fileName = emittedFile ?
path.join(path.dirname(emittedFile), path.basename(file.genFileUrl)) :
file.genFileUrl;
this.host.writeFile(fileName, file.source, false, error => {});
}
});
return result;
}
// Private members
private get analyzedModules(): NgAnalyzedModules {
return this._analyzedModules || (this._analyzedModules = this.analyzeModules());
}
private get structuralDiagnostics(): Diagnostic[] {
return this.analyzedModules && this._structuralDiagnostics
}
private get stubs(): GeneratedFile[] {
return this._stubs || (this._stubs = this.generateStubs());
}
private get stubFiles(): string[] {
return this._stubFiles ||
(this._stubFiles = this.stubs.reduce((files: string[], generatedFile) => {
if (generatedFile.source || (generatedFile.stmts && generatedFile.stmts.length)) {
return [...files, generatedFile.genFileUrl];
}
return files;
}, []));
}
private get programWithStubsHost(): ts.CompilerHost {
return this._programWithStubsHost || (this._programWithStubsHost = createProgramWithStubsHost(
this.stubs, this.tsProgram, this.host));
}
private get programWithStubs(): ts.Program {
return this._programWithStubs || (this._programWithStubs = this.createProgramWithStubs());
}
private get generatedFiles(): GeneratedFile[] {
return this._generatedFiles || (this._generatedFiles = this.generateFiles())
}
private get typeChecker(): TypeChecker {
return (this._typeChecker && !this._typeChecker.partialResults) ?
this._typeChecker :
(this._typeChecker = this.createTypeChecker());
}
private get generatedFileDiagnostics(): Diagnostic[]|undefined {
return this.generatedFiles && this._generatedFileDiagnostics !;
}
private catchAnalysisError(e: any): NgAnalyzedModules {
if (isSyntaxError(e)) {
const parserErrors = getParseErrors(e);
if (parserErrors && parserErrors.length) {
this._structuralDiagnostics =
parserErrors.map<Diagnostic>(e => ({
message: e.contextualMessage(),
category: DiagnosticCategory.Error,
span: e.span
}));
} else {
this._structuralDiagnostics = [{message: e.message, category: DiagnosticCategory.Error}];
}
this._analyzedModules = emptyModules;
return emptyModules;
}
throw e;
}
private analyzeModules() {
try {
return this.compiler.analyzeModulesSync(this.srcNames);
} catch (e) {
return this.catchAnalysisError(e);
}
}
private generateStubs() {
return this.options.skipTemplateCodegen ? [] :
this.options.generateCodeForLibraries === false ?
this.compiler.emitAllStubs(this.analyzedModules) :
this.compiler.emitPartialStubs(this.analyzedModules);
}
private generateFiles() {
try {
// Always generate the files if requested to ensure we capture any diagnostic errors but only
// keep the results if we are not skipping template code generation.
const result = this.compiler.emitAllImpls(this.analyzedModules);
return this.options.skipTemplateCodegen ? [] : result;
} catch (e) {
if (isSyntaxError(e)) {
this._generatedFileDiagnostics = [{message: e.message, category: DiagnosticCategory.Error}];
return [];
}
throw e;
}
}
private createTypeChecker(): TypeChecker {
return new TypeChecker(
this.tsProgram, this.options, this.host, this.aotCompilerHost, this.options,
this.analyzedModules, this.generatedFiles);
}
private createProgramWithStubs(): ts.Program {
// If we are skipping code generation just use the original program.
// Otherwise, create a new program that includes the stub files.
return this.options.skipTemplateCodegen ?
this.tsProgram :
ts.createProgram(
[...this.rootNames, ...this.stubFiles], this.options, this.programWithStubsHost);
}
}
export function createProgram(
{rootNames, options, host, oldProgram}:
{rootNames: string[], options: CompilerOptions, host: CompilerHost, oldProgram?: Program}):
Program {
return new AngularCompilerProgram(rootNames, options, host, oldProgram);
}
function writeMetadata(
emitFilePath: string, sourceFile: ts.SourceFile, collector: MetadataCollector,
ngOptions: CompilerOptions) {
if (/\.js$/.test(emitFilePath)) {
const path = emitFilePath.replace(/\.js$/, '.metadata.json');
// Beginning with 2.1, TypeScript transforms the source tree before emitting it.
// We need the original, unmodified, tree which might be several levels back
// depending on the number of transforms performed. All SourceFile's prior to 2.1
// will appear to be the original source since they didn't include an original field.
let collectableFile = sourceFile;
while ((collectableFile as any).original) {
collectableFile = (collectableFile as any).original;
}
const metadata = collector.getMetadata(collectableFile, !!ngOptions.strictMetadataEmit);
if (metadata) {
const metadataText = JSON.stringify([metadata]);
writeFileSync(path, metadataText, {encoding: 'utf-8'});
}
}
}
function createWriteFileCallback(
emitFlags: EmitFlags, host: ts.CompilerHost, collector: MetadataCollector,
ngOptions: CompilerOptions, emitMap: Map<string, string>) {
const withMetadata =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
const generatedFile = GENERATED_FILES.test(fileName);
if (!generatedFile || data != '') {
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
}
if (!generatedFile && sourceFiles && sourceFiles.length == 1) {
emitMap.set(sourceFiles[0].fileName, fileName);
writeMetadata(fileName, sourceFiles[0], collector, ngOptions);
}
};
const withoutMetadata =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
const generatedFile = GENERATED_FILES.test(fileName);
if (!generatedFile || data != '') {
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
}
if (!generatedFile && sourceFiles && sourceFiles.length == 1) {
emitMap.set(sourceFiles[0].fileName, fileName);
}
};
return (emitFlags & EmitFlags.Metadata) != 0 ? withMetadata : withoutMetadata;
}
function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
if (options.annotationsAs) {
switch (options.annotationsAs) {
case 'decorators':
case 'static fields':
break;
default:
return [{
message:
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
category: DiagnosticCategory.Error
}];
}
}
return [];
}
function createProgramWithStubsHost(
generatedFiles: GeneratedFile[], originalProgram: ts.Program,
originalHost: ts.CompilerHost): ts.CompilerHost {
interface FileData {
g: GeneratedFile
s?: ts.SourceFile;
}
return new class implements ts.CompilerHost {
private generatedFiles: Map<string, FileData>;
writeFile: ts.WriteFileCallback;
getCancellationToken: () => ts.CancellationToken;
getDefaultLibLocation: () => string;
trace: (s: string) => void;
getDirectories: (path: string) => string[];
directoryExists: (directoryName: string) => boolean;
constructor() {
this.generatedFiles =
new Map(generatedFiles.filter(g => g.source || (g.stmts && g.stmts.length))
.map<[string, FileData]>(g => [g.genFileUrl, {g}]));
this.writeFile = originalHost.writeFile;
if (originalHost.getDirectories) {
this.getDirectories = path => originalHost.getDirectories !(path);
}
if (originalHost.directoryExists) {
this.directoryExists = directoryName => originalHost.directoryExists !(directoryName);
}
if (originalHost.getCancellationToken) {
this.getCancellationToken = () => originalHost.getCancellationToken !();
}
if (originalHost.getDefaultLibLocation) {
this.getDefaultLibLocation = () => originalHost.getDefaultLibLocation !();
}
if (originalHost.trace) {
this.trace = s => originalHost.trace !(s);
}
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
const data = this.generatedFiles.get(fileName);
if (data) {
return data.s || (data.s = ts.createSourceFile(
fileName, data.g.source || toTypeScript(data.g), languageVersion));
}
return originalProgram.getSourceFile(fileName) ||
originalHost.getSourceFile(fileName, languageVersion, onError);
}
readFile(fileName: string): string {
const data = this.generatedFiles.get(fileName);
if (data) {
return data.g.source || toTypeScript(data.g);
}
return originalHost.readFile(fileName);
}
getDefaultLibFileName = (options: ts.CompilerOptions) =>
originalHost.getDefaultLibFileName(options);
getCurrentDirectory = () => originalHost.getCurrentDirectory();
getCanonicalFileName = (fileName: string) => originalHost.getCanonicalFileName(fileName);
useCaseSensitiveFileNames = () => originalHost.useCaseSensitiveFileNames();
getNewLine = () => originalHost.getNewLine();
fileExists = (fileName: string) =>
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
};
}