/** * @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 { 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(); 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(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) { 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; 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(); realPath = (p: string) => p; fileExists = (fileName: string) => this.generatedFiles.has(fileName) || originalHost.fileExists(fileName); }; }