perf(compiler): make the creation of ts.Program faster. (#19275)

We now create 2 programs with exactly the same fileNames and
exactly the same `import` / `export` declarations,
allowing TS to reuse the structure of first program
completely. When passing in an oldProgram and the files didn’t change,
TS can also reuse the old program completely.

This is possible buy adding generated files to TS
in `host.geSourceFile` via `ts.SourceFile.referencedFiles`.

This commit also:
- has a minor side effect on how we generate shared stylesheets:
  - previously every import in a stylesheet would generate a new
    `.ngstyles.ts` file.
  - now, we only generate 1 `.ngstyles.ts` file per entry in `@Component.styleUrls`.
  This was required as we need to be able to determine the program files
  without loading the resources (which can be async).
- makes all angular related methods in `CompilerHost`
  optional, allowing to just use a regular `ts.CompilerHost` as `CompilerHost`.
- simplifies the logic around `Compiler.analyzeNgModules` by introducing `NgAnalyzedFile`.

Perf impact: 1.5s improvement in compiling angular io
PR Close #19275
This commit is contained in:
Tobias Bosch
2017-09-12 09:40:28 -07:00
committed by Igor Minar
parent 9d2236a4b5
commit edd5f5a333
35 changed files with 1929 additions and 1536 deletions

View File

@ -22,8 +22,6 @@ const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsummary\.ts$/;
const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsummary\.ts$/;
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; }
export interface BaseAotCompilerHostContext extends ts.ModuleResolutionHost {
readResource?(fileName: string): Promise<string>|string;
}
@ -35,9 +33,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
private flatModuleIndexNames = new Set<string>();
private flatModuleIndexRedirectNames = new Set<string>();
constructor(
protected program: ts.Program, protected options: CompilerOptions, protected context: C,
protected metadataProvider: MetadataProvider = new MetadataCollector()) {}
constructor(protected options: CompilerOptions, protected context: C) {}
abstract moduleNameToFileName(m: string, containingFile: string): string|null;
@ -49,17 +45,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
abstract fromSummaryFileName(fileName: string, referringLibFileName: string): string;
protected getSourceFile(filePath: string): ts.SourceFile {
const sf = this.program.getSourceFile(filePath);
if (!sf) {
if (this.context.fileExists(filePath)) {
const sourceText = this.context.readFile(filePath);
return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
}
throw new Error(`Source file ${filePath} not present in program.`);
}
return sf;
}
abstract getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined;
getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
if (!this.context.fileExists(filePath)) {
@ -82,8 +68,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
}
}
const sf = this.getSourceFile(filePath);
const metadata = this.metadataProvider.getMetadata(sf);
const metadata = this.getMetadataForSourceFile(filePath);
return metadata ? [metadata] : [];
}
@ -122,7 +107,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
}
const exports = this.metadataProvider.getMetadata(this.getSourceFile(dtsFilePath));
const exports = this.getMetadataForSourceFile(dtsFilePath);
if (exports) {
for (let prop in exports.metadata) {
if (!v3Metadata.metadata[prop]) {
@ -233,6 +218,7 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost {
// TODO(tbosch): remove this once G3 uses the transformer compiler!
export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
protected metadataProvider: MetadataCollector;
protected basePath: string;
private moduleFileNames = new Map<string, string|null>();
private isGenDirChildOfRootDir: boolean;
@ -241,10 +227,10 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
private urlResolver: UrlResolver;
constructor(
program: ts.Program, options: CompilerOptions, context: CompilerHostContext,
collectorOptions?: CollectorOptions,
metadataProvider: MetadataProvider = new MetadataCollector(collectorOptions)) {
super(program, options, context, metadataProvider);
protected program: ts.Program, options: CompilerOptions, context: CompilerHostContext,
collectorOptions?: CollectorOptions) {
super(options, context);
this.metadataProvider = new MetadataCollector(collectorOptions);
// normalize the path so that it never ends with '/'.
this.basePath = path.normalize(path.join(this.options.basePath !, '.')).replace(/\\/g, '/');
this.genDir = path.normalize(path.join(this.options.genDir !, '.')).replace(/\\/g, '/');
@ -274,6 +260,19 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
this.urlResolver = createOfflineCompileUrlResolver();
}
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
let sf = this.program.getSourceFile(filePath);
if (!sf) {
if (this.context.fileExists(filePath)) {
const sourceText = this.context.readFile(filePath);
sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
} else {
throw new Error(`Source file ${filePath} not present in program.`);
}
}
return this.metadataProvider.getMetadata(sf);
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return fileName.replace(EXT, '') + '.d.ts';
}

View File

@ -13,7 +13,7 @@ import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from '../transformers/api';
import {GENERATED_FILES} from '../transformers/util';
export interface TypeCheckHost {
ngSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null;
parseSourceSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null;
}
export function translateDiagnostics(host: TypeCheckHost, untranslatedDiagnostics: ts.Diagnostic[]):
@ -49,7 +49,7 @@ export function translateDiagnostics(host: TypeCheckHost, untranslatedDiagnostic
function sourceSpanOf(host: TypeCheckHost, source: ts.SourceFile, start: number): ParseSourceSpan|
null {
const {line, character} = ts.getLineAndCharacterOfPosition(source, start);
return host.ngSpanOf(source.fileName, line, character);
return host.parseSourceSpanOf(source.fileName, line, character);
}
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, extractProgramSymbols} from '@angular/compiler';
import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';

View File

@ -14,7 +14,7 @@ Angular modules and Typescript as this will indirectly add a dependency
to the language service.
*/
export {CompilerHost, CompilerHostContext, MetadataProvider, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './compiler_host';
export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './compiler_host';
export {DiagnosticTemplateInfo, ExpressionDiagnostic, getExpressionDiagnostics, getExpressionScope, getTemplateExpressionDiagnostics} from './diagnostics/expression_diagnostics';
export {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './diagnostics/expression_type';
export {BuiltinType, DeclarationKind, Definition, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './diagnostics/symbols';

View File

@ -55,11 +55,11 @@ export interface CompilerOptions extends ts.CompilerOptions {
}
export interface CompilerHost extends ts.CompilerHost {
moduleNameToFileName(moduleName: string, containingFile?: string): string|null;
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
resourceNameToFileName(resourceName: string, containingFilePath: string): string|null;
toSummaryFileName(fileName: string, referringSrcFileName: string): string;
fromSummaryFileName(fileName: string, referringLibFileName: string): string;
moduleNameToFileName?(moduleName: string, containingFile?: string): string|null;
fileNameToModuleName?(importedFilePath: string, containingFilePath: string): string;
resourceNameToFileName?(resourceName: string, containingFilePath: string): string|null;
toSummaryFileName?(fileName: string, referringSrcFileName: string): string;
fromSummaryFileName?(fileName: string, referringLibFileName: string): string;
readResource?(fileName: string): Promise<string>|string;
}

View File

@ -131,9 +131,7 @@ export class PathMappedCompilerHost extends CompilerHost {
return this.readMetadata(metadataPath, rootedPath);
}
} else {
const sf = this.getSourceFile(rootedPath);
sf.fileName = sf.fileName;
const metadata = this.metadataProvider.getMetadata(sf);
const metadata = this.getMetadataForSourceFile(rootedPath);
return metadata ? [metadata] : [];
}
}

View File

@ -141,12 +141,6 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa
customTransformers?: api.CustomTransformers,
emitFlags?: api.EmitFlags
}): PerformCompilationResult {
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
let program: api.Program|undefined;
let emitResult: ts.EmitResult|undefined;
let allDiagnostics: Diagnostics = [];

View File

@ -53,8 +53,8 @@ export interface PerformWatchHost {
export function createPerformWatchHost(
configFileName: string, reportDiagnostics: (diagnostics: Diagnostics) => void,
existingOptions?: ts.CompilerOptions,
createEmitCallback?: (options: api.CompilerOptions) => api.TsEmitCallback): PerformWatchHost {
existingOptions?: ts.CompilerOptions, createEmitCallback?: (options: api.CompilerOptions) =>
api.TsEmitCallback | undefined): PerformWatchHost {
return {
reportDiagnostics: reportDiagnostics,
createCompilerHost: options => createCompilerHost({options}),

View File

@ -135,17 +135,17 @@ export interface CompilerHost extends ts.CompilerHost {
* 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;
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`.
*/
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
fileNameToModuleName?(importedFilePath: string, containingFilePath: string): string;
/**
* Converts a file path for a resource that is used in a source file or another resource
* into a filepath.
*/
resourceNameToFileName(resourceName: string, containingFilePath: string): string|null;
resourceNameToFileName?(resourceName: string, containingFilePath: string): string|null;
/**
* Converts a file name into a representation that should be stored in a summary file.
* This has to include changing the suffix as well.
@ -154,12 +154,12 @@ export interface CompilerHost extends ts.CompilerHost {
*
* @param referringSrcFileName the soure file that refers to fileName
*/
toSummaryFileName(fileName: string, referringSrcFileName: string): string;
toSummaryFileName?(fileName: string, referringSrcFileName: string): string;
/**
* Converts a fileName that was processed by `toSummaryFileName` back into a real fileName
* given the fileName of the library that is referrig to it.
*/
fromSummaryFileName(fileName: string, referringLibFileName: string): string;
fromSummaryFileName?(fileName: string, referringLibFileName: string): string;
/**
* 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
@ -267,7 +267,7 @@ export interface Program {
*
* Angular structural information is required to emit files.
*/
emit({emitFlags, cancellationToken, customTransformers, emitCallback}: {
emit({emitFlags, cancellationToken, customTransformers, emitCallback}?: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,

View File

@ -6,11 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {syntaxError} from '@angular/compiler';
import {EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {BaseAotCompilerHost} from '../compiler_host';
import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata} from '../metadata/index';
import {CompilerHost, CompilerOptions} from './api';
import {GENERATED_FILES} from './util';
const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-)+|(@(\w|-)+\/(\w|-)+))/;
const DTS = /\.d\.ts$/;
@ -19,58 +24,122 @@ const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
export function createCompilerHost(
{options, tsHost = ts.createCompilerHost(options, true)}:
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
const mixin = new CompilerHostMixin(tsHost, options);
const host = Object.create(tsHost);
host.moduleNameToFileName = mixin.moduleNameToFileName.bind(mixin);
host.fileNameToModuleName = mixin.fileNameToModuleName.bind(mixin);
host.toSummaryFileName = mixin.toSummaryFileName.bind(mixin);
host.fromSummaryFileName = mixin.fromSummaryFileName.bind(mixin);
host.resourceNameToFileName = mixin.resourceNameToFileName.bind(mixin);
// 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;
return tsHost;
}
class CompilerHostMixin {
private rootDirs: string[];
private basePath: string;
private moduleResolutionHost: ModuleFilenameResolutionHost;
private moduleResolutionCache: ts.ModuleResolutionCache;
export interface MetadataProvider {
getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined;
}
constructor(private context: ts.CompilerHost, private options: CompilerOptions) {
// normalize the path so that it never ends with '/'.
this.basePath = normalizePath(this.options.basePath !);
this.rootDirs = (this.options.rootDirs || [
this.options.basePath !
]).map(p => path.resolve(this.basePath, normalizePath(p)));
this.moduleResolutionHost = createModuleFilenameResolverHost(context);
interface GenSourceFile {
externalReferences: Set<string>;
sourceFile: ts.SourceFile;
emitCtx: EmitterVisitorContext;
}
/**
* Implements the following hosts based on an api.CompilerHost:
* - ts.CompilerHost to be consumed by a ts.Program
* - AotCompilerHost for @angular/compiler
* - TypeCheckHost for mapping ts errors to ng errors (via translateDiagnostics)
*/
export class TsCompilerAotCompilerTypeCheckHostAdapter extends
BaseAotCompilerHost<CompilerHost> implements ts.CompilerHost,
TypeCheckHost {
private rootDirs: string[];
private moduleResolutionCache: ts.ModuleResolutionCache;
private originalSourceFiles = new Map<string, ts.SourceFile>();
private generatedSourceFiles = new Map<string, GenSourceFile>();
private generatedCodeFor = new Set<string>();
private emitter = new TypeScriptEmitter();
getCancellationToken: () => ts.CancellationToken;
getDefaultLibLocation: () => string;
trace: (s: string) => void;
getDirectories: (path: string) => string[];
directoryExists?: (directoryName: string) => boolean;
constructor(
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
private metadataProvider: MetadataProvider,
private codeGenerator: (fileName: string) => GeneratedFile[]) {
super(options, context);
this.moduleResolutionCache = ts.createModuleResolutionCache(
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
const basePath = this.options.basePath !;
this.rootDirs =
(this.options.rootDirs || [this.options.basePath !]).map(p => path.resolve(basePath, p));
if (context.getDirectories) {
this.getDirectories = path => context.getDirectories !(path);
}
if (context.directoryExists) {
this.directoryExists = directoryName => context.directoryExists !(directoryName);
}
if (context.getCancellationToken) {
this.getCancellationToken = () => context.getCancellationToken !();
}
if (context.getDefaultLibLocation) {
this.getDefaultLibLocation = () => context.getDefaultLibLocation !();
}
if (context.trace) {
this.trace = s => context.trace !(s);
}
if (context.fileNameToModuleName) {
this.fileNameToModuleName = context.fileNameToModuleName.bind(context);
}
// Note: don't copy over context.moduleNameToFileName as we first
// normalize undefined containingFile to a filled containingFile.
if (context.resourceNameToFileName) {
this.resourceNameToFileName = context.resourceNameToFileName.bind(context);
}
if (context.toSummaryFileName) {
this.toSummaryFileName = context.toSummaryFileName.bind(context);
}
if (context.fromSummaryFileName) {
this.fromSummaryFileName = context.fromSummaryFileName.bind(context);
}
}
moduleNameToFileName(m: string, containingFile: string): string|null {
private resolveModuleName(moduleName: string, containingFile: string): ts.ResolvedModule
|undefined {
const rm = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.moduleResolutionCache)
.resolvedModule;
if (rm && this.isSourceFile(rm.resolvedFileName)) {
// Case: generateCodeForLibraries = true and moduleName is
// a .d.ts file in a node_modules folder.
// Need to set isExternalLibraryImport to false so that generated files for that file
// are emitted.
rm.isExternalLibraryImport = false;
}
return rm;
}
// Note: We implement this method so that TypeScript and Angular share the same
// ts.ModuleResolutionCache
// and that we can tell ts.Program about our different opinion about
// ResolvedModule.isExternalLibraryImport
// (see our isSourceFile method).
resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] {
// TODO(tbosch): this seems to be a typing error in TypeScript,
// as it contains assertions that the result contains the same number of entries
// as the given module names.
return <ts.ResolvedModule[]>moduleNames.map(
moduleName => this.resolveModuleName(moduleName, containingFile));
}
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 = path.join(this.basePath, 'index.ts');
containingFile = this.rootFiles[0];
}
const resolved = ts.resolveModuleName(
m, containingFile.replace(/\\/g, '/'), this.options,
this.moduleResolutionHost, this.moduleResolutionCache)
.resolvedModule;
if (resolved) {
if (this.options.traceResolution) {
console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName);
}
return resolved.resolvedFileName;
if (this.context.moduleNameToFileName) {
return this.context.moduleNameToFileName(m, containingFile);
}
return null;
const resolved = this.resolveModuleName(m, containingFile);
return resolved ? resolved.resolvedFileName : null;
}
/**
@ -97,11 +166,6 @@ class CompilerHostMixin {
'fileNameToModuleName 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 it so that the `resolve` method works!
if (!this.moduleResolutionHost.fileExists(importedFile)) {
this.moduleResolutionHost.assumeFileExists(importedFile);
}
// drop extension
importedFile = importedFile.replace(EXT, '');
const importedFilePackagName = getPackageName(importedFile);
@ -109,8 +173,8 @@ class CompilerHostMixin {
let moduleName: string;
if (importedFilePackagName === containingFilePackageName) {
const rootedContainingFile = stripRootDir(this.rootDirs, containingFile);
const rootedImportedFile = stripRootDir(this.rootDirs, importedFile);
const rootedContainingFile = relativeToRootDirs(containingFile, this.rootDirs);
const rootedImportedFile = relativeToRootDirs(importedFile, this.rootDirs);
if (rootedContainingFile !== containingFile && rootedImportedFile !== importedFile) {
// if both files are contained in the `rootDirs`, then strip the rootDirs
@ -127,18 +191,6 @@ class CompilerHostMixin {
return moduleName;
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return this.fileNameToModuleName(fileName, referringSrcFileName);
}
fromSummaryFileName(fileName: string, referringLibFileName: string): string {
const resolved = this.moduleNameToFileName(fileName, referringLibFileName);
if (!resolved) {
throw new Error(`Could not resolve ${fileName} from ${referringLibFileName}`);
}
return resolved;
}
resourceNameToFileName(resourceName: string, containingFile: string): string|null {
// Note: we convert package paths into relative paths to be compatible with the the
// previous implementation of UrlResolver.
@ -152,45 +204,193 @@ class CompilerHostMixin {
this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile);
return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null;
}
}
interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost {
assumeFileExists(fileName: string): void;
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return this.fileNameToModuleName(fileName, referringSrcFileName);
}
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 DefaultModuleFilenameResolver
// and it should be able to resolve summary file names.
resolveModuleNameHost.fileExists = (fileName: string): boolean => {
fromSummaryFileName(fileName: string, referringLibFileName: string): string {
const resolved = this.moduleNameToFileName(fileName, referringLibFileName);
if (!resolved) {
throw new Error(`Could not resolve ${fileName} from ${referringLibFileName}`);
}
return resolved;
}
parseSourceSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null {
const data = this.generatedSourceFiles.get(fileName);
if (data && data.emitCtx) {
return data.emitCtx.spanOf(line, character);
}
return null;
}
private getOriginalSourceFile(
filePath: string, languageVersion?: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile|undefined {
let sf = this.originalSourceFiles.get(filePath);
if (sf) {
return sf;
}
if (!languageVersion) {
languageVersion = this.options.target || ts.ScriptTarget.Latest;
}
sf = this.context.getSourceFile(filePath, languageVersion, onError);
this.originalSourceFiles.set(filePath, sf);
return sf;
}
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
const sf = this.getOriginalSourceFile(filePath);
if (!sf) {
return undefined;
}
return this.metadataProvider.getMetadata(sf);
}
updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile|null {
if (!genFile.stmts) {
return null;
}
const oldGenFile = this.generatedSourceFiles.get(genFile.genFileUrl);
if (!oldGenFile) {
throw new Error(`Illegal State: previous GeneratedFile not found for ${genFile.genFileUrl}.`);
}
const newRefs = genFileExternalReferences(genFile);
const oldRefs = oldGenFile.externalReferences;
let refsAreEqual = oldRefs.size === newRefs.size;
if (refsAreEqual) {
newRefs.forEach(r => refsAreEqual = refsAreEqual && oldRefs.has(r));
}
if (!refsAreEqual) {
throw new Error(
`Illegal State: external references changed in ${genFile.genFileUrl}.\nOld: ${Array.from(oldRefs)}.\nNew: ${Array.from(newRefs)}`);
}
return this.addGeneratedFile(genFile, newRefs);
}
private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set<string>): ts.SourceFile
|null {
if (!genFile.stmts) {
return null;
}
const {sourceText, context} = this.emitter.emitStatementsAndContext(
genFile.srcFileUrl, genFile.genFileUrl, genFile.stmts, /* preamble */ '',
/* emitSourceMaps */ false);
const sf = ts.createSourceFile(
genFile.genFileUrl, sourceText, this.options.target || ts.ScriptTarget.Latest);
this.generatedSourceFiles.set(genFile.genFileUrl, {
sourceFile: sf,
emitCtx: context, externalReferences,
});
return sf;
}
private ensureCodeGeneratedFor(fileName: string): void {
if (this.generatedCodeFor.has(fileName)) {
return;
}
this.generatedCodeFor.add(fileName);
const baseNameFromGeneratedFile = this._getBaseNameForGeneratedFile(fileName);
if (baseNameFromGeneratedFile) {
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
}
const sf = this.getOriginalSourceFile(fileName, this.options.target || ts.ScriptTarget.Latest);
if (!sf) {
return;
}
const genFileNames: string[] = [];
if (this.isSourceFile(fileName)) {
// Note: we can't exit early here,
// as we might need to clear out old changes to `SourceFile.referencedFiles`
// that were created by a previous run, given an original CompilerHost
// that caches source files.
const genFiles = this.codeGenerator(fileName);
genFiles.forEach(genFile => {
const sf = this.addGeneratedFile(genFile, genFileExternalReferences(genFile));
if (sf) {
genFileNames.push(sf.fileName);
}
});
}
addReferencesToSourceFile(sf, genFileNames);
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
this.ensureCodeGeneratedFor(fileName);
const genFile = this.generatedSourceFiles.get(fileName);
if (genFile) {
return genFile.sourceFile;
}
// TODO(tbosch): TypeScript's typings for getSourceFile are incorrect,
// as it can very well return undefined.
return this.getOriginalSourceFile(fileName, languageVersion, onError) !;
}
fileExists(fileName: string): boolean {
fileName = stripNgResourceSuffix(fileName);
if (assumedExists.has(fileName)) {
// Note: Don't rely on this.generatedSourceFiles here,
// as it might not have been filled yet.
if (this._getBaseNameForGeneratedFile(fileName)) {
return true;
}
return this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
}
if (host.fileExists(fileName)) {
return true;
private _getBaseNameForGeneratedFile(genFileName: string): string|null {
const genMatch = GENERATED_FILES.exec(genFileName);
if (genMatch) {
const [, base, genSuffix, suffix] = genMatch;
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// (see options.generateCodeForLibraries).
// It can also be a `.css` file in case of a `.css.ngstyle.ts` file
if (suffix === 'ts') {
const baseNames =
genSuffix.indexOf('ngstyle') >= 0 ? [base] : [`${base}.ts`, `${base}.d.ts`];
return baseNames.find(
baseName => this.isSourceFile(baseName) && this.fileExists(baseName)) ||
null;
}
}
return null;
}
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);
readFile = (fileName: string) => this.context.readFile(fileName);
getDefaultLibFileName = (options: ts.CompilerOptions) =>
this.context.getDefaultLibFileName(options);
getCurrentDirectory = () => this.context.getCurrentDirectory();
getCanonicalFileName = (fileName: string) => this.context.getCanonicalFileName(fileName);
useCaseSensitiveFileNames = () => this.context.useCaseSensitiveFileNames();
getNewLine = () => this.context.getNewLine();
// 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;
realPath = (p: string) => p;
writeFile = this.context.writeFile.bind(this.context);
}
return resolveModuleNameHost;
function genFileExternalReferences(genFile: GeneratedFile): Set<string> {
return new Set(collectExternalReferences(genFile.stmts !).map(er => er.moduleName !));
}
function addReferencesToSourceFile(sf: ts.SourceFile, genFileNames: string[]) {
// Note: as we modify ts.SourceFiles we need to keep the original
// value for `referencedFiles` around in cache the original host is caching ts.SourceFiles.
// Note: cloning the ts.SourceFile is expensive as the nodes in have parent pointers,
// i.e. we would also need to clone and adjust all nodes.
let originalReferencedFiles: ts.FileReference[]|undefined = (sf as any).originalReferencedFiles;
if (!originalReferencedFiles) {
originalReferencedFiles = sf.referencedFiles;
(sf as any).originalReferencedFiles = originalReferencedFiles;
}
const newReferencedFiles = [...originalReferencedFiles];
genFileNames.forEach(gf => newReferencedFiles.push({fileName: gf, pos: 0, end: 0}));
sf.referencedFiles = newReferencedFiles;
}
function dotRelative(from: string, to: string): string {
@ -206,16 +406,13 @@ function getPackageName(filePath: string): string|null {
return match ? match[1] : null;
}
function stripRootDir(rootDirs: string[], fileName: string): string {
if (!fileName) return fileName;
// NB: the rootDirs should have been sorted longest-first
for (const dir of rootDirs) {
if (fileName.indexOf(dir) === 0) {
fileName = fileName.substring(dir.length);
break;
}
export function relativeToRootDirs(filePath: string, rootDirs: string[]): string {
if (!filePath) return filePath;
for (const dir of rootDirs || []) {
const rel = path.relative(dir, filePath);
if (rel.indexOf('.') != 0) return rel;
}
return fileName;
return filePath;
}
function stripNodeModulesPrefix(filePath: string): string {
@ -227,10 +424,6 @@ function getNodeModulesPrefix(filePath: string): string|null {
return match ? match[1] : null;
}
function normalizePath(p: string): string {
return path.normalize(path.join(p, '.')).replace(/\\/g, '/');
}
function stripNgResourceSuffix(fileName: string): string {
return fileName.replace(/\.\$ngresource\$.*/, '');
}

View File

@ -6,19 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedModules, ParseSourceSpan, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Serializer, StubEmitFlags, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {BaseAotCompilerHost} from '../compiler_host';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {createBundleIndexHost} from '../metadata/index';
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {TsCompilerAotCompilerTypeCheckHostAdapter} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {GENERATED_FILES} from './util';
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from './util';
const emptyModules: NgAnalyzedModules = {
ngModules: [],
@ -34,17 +34,13 @@ const defaultEmitCallback: TsEmitCallback =
class AngularCompilerProgram implements Program {
private tsProgram: ts.Program;
private aotCompilerHost: AotCompilerHost;
private compiler: AotCompiler;
private srcNames: string[];
private metadataCache: LowerMetadataCache;
// Lazily initialized fields
private _typeCheckHost: TypeCheckHost;
private _compiler: AotCompiler;
private _tsProgram: ts.Program;
private _analyzedModules: NgAnalyzedModules|undefined;
private _structuralDiagnostics: Diagnostic[] = [];
private _stubs: GeneratedFile[]|undefined;
private _stubFiles: string[]|undefined;
private _programWithStubsHost: ts.CompilerHost&TypeCheckHost|undefined;
private _structuralDiagnostics: Diagnostic[]|undefined;
private _programWithStubs: ts.Program|undefined;
private _generatedFiles: GeneratedFile[]|undefined;
private _generatedFileDiagnostics: Diagnostic[]|undefined;
@ -53,7 +49,13 @@ class AngularCompilerProgram implements Program {
constructor(
private rootNames: string[], private options: CompilerOptions, private host: CompilerHost,
oldProgram?: Program) {
private oldProgram?: Program) {
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r));
if (options.flatModuleOutFile) {
const {host: bundleHost, indexName, errors} = createBundleIndexHost(options, rootNames, host);
if (errors) {
@ -67,26 +69,13 @@ class AngularCompilerProgram implements Program {
})));
} else {
rootNames.push(indexName !);
this.host = host = bundleHost;
this.host = bundleHost;
}
}
const oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
this.tsProgram = ts.createProgram(rootNames, options, host, oldTsProgram);
this.srcNames =
this.tsProgram.getSourceFiles()
.map(sf => sf.fileName)
.filter(f => !f.match(/\.ngfactory\.[\w.]+$|\.ngstyle\.[\w.]+$|\.ngsummary\.[\w.]+$/));
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
this.aotCompilerHost =
new AotCompilerHostImpl(this.tsProgram, options, host, this.metadataCache);
const aotOptions = getAotCompilerOptions(options);
this.compiler = createAotCompiler(this.aotCompilerHost, aotOptions).compiler;
}
// Program implementation
getTsProgram(): ts.Program { return this.programWithStubs; }
getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
@ -121,23 +110,28 @@ class AngularCompilerProgram implements Program {
}
loadNgStructureAsync(): Promise<void> {
return this.compiler.analyzeModulesAsync(this.rootNames)
if (this._analyzedModules) {
throw new Error('Angular structure already loaded');
}
const {tmpProgram, analyzedFiles, hostAdapter} = this._createProgramWithBasicStubs();
return this._compiler.loadFilesAsync(analyzedFiles)
.catch(this.catchAnalysisError.bind(this))
.then(analyzedModules => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronsly');
}
this._analyzedModules = analyzedModules;
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter);
});
}
emit({emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
}): ts.EmitResult {
emit(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
} = {}): ts.EmitResult {
if (emitFlags & EmitFlags.I18nBundle) {
const locale = this.options.i18nOutLocale || null;
const file = this.options.i18nOutFile || null;
@ -145,60 +139,95 @@ class AngularCompilerProgram implements Program {
const bundle = this.compiler.emitMessageBundle(this.analyzedModules, locale);
i18nExtract(format, file, this.host, this.options, bundle);
}
if (emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Summary)) {
return emitCallback({
program: this.programWithStubs,
host: this.host,
options: this.options,
targetSourceFile: undefined,
writeFile:
createWriteFileCallback(emitFlags, this.host, this.metadataCache, this.generatedFiles),
cancellationToken,
emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
customTransformers: this.calculateTransforms(customTransformers)
const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = [];
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Summary)) ===
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
const emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
targetSourceFile: undefined,
writeFile: createWriteFileCallback(this.options, this.host, outSrcMapping), cancellationToken,
emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
customTransformers: this.calculateTransforms(customTransformers)
});
if (!outSrcMapping.length) {
// if no files were emitted by TypeScript, also don't emit .json files
return emitResult;
}
const srcToOutPath = this.createSrcToOutPathMapper(outSrcMapping);
if (!this.options.skipTemplateCodegen) {
this.generatedFiles.forEach(gf => {
if (gf.source) {
this.host.writeFile(srcToOutPath(gf.genFileUrl), gf.source, false);
}
});
}
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
if (emitFlags & EmitFlags.Metadata) {
this.tsProgram.getSourceFiles().forEach(sf => {
if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) {
const metadata = this.metadataCache.getMetadata(sf);
const metadataText = JSON.stringify([metadata]);
this.host.writeFile(
srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json')), metadataText, false);
}
});
}
return emitResult;
}
// Private members
private get compiler(): AotCompiler {
if (!this._compiler) {
this.initSync();
}
return this._compiler !;
}
private get analyzedModules(): NgAnalyzedModules {
return this._analyzedModules || (this._analyzedModules = this.analyzeModules());
if (!this._analyzedModules) {
this.initSync();
}
return this._analyzedModules !;
}
private get structuralDiagnostics(): Diagnostic[] {
return this.analyzedModules && this._structuralDiagnostics;
if (!this._structuralDiagnostics) {
this.initSync();
}
return this._structuralDiagnostics !;
}
private get stubs(): GeneratedFile[] {
return this._stubs || (this._stubs = this.generateStubs());
private get tsProgram(): ts.Program {
if (!this._tsProgram) {
this.initSync();
}
return this._tsProgram !;
}
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&TypeCheckHost {
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 typeCheckHost(): TypeCheckHost {
if (!this._typeCheckHost) {
this.initSync();
}
return this._typeCheckHost !;
}
private get generatedFiles(): GeneratedFile[] {
return this._generatedFiles || (this._generatedFiles = this.generateFiles());
if (!this._generatedFiles) {
this.generateFiles();
}
return this._generatedFiles !;
}
private get generatedFileDiagnostics(): Diagnostic[]|undefined {
return this.generatedFiles && this._generatedFileDiagnostics !;
if (!this._generatedFileDiagnostics) {
this.generateFiles();
}
return this._generatedFileDiagnostics !;
}
private get semanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
@ -211,9 +240,7 @@ class AngularCompilerProgram implements Program {
if (!this.options.disableExpressionLowering) {
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache));
}
if (!this.options.skipTemplateCodegen) {
beforeTs.push(getAngularEmitterTransformFactory(this.generatedFiles));
}
beforeTs.push(getAngularEmitterTransformFactory(this.generatedFiles));
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
}
@ -221,6 +248,91 @@ class AngularCompilerProgram implements Program {
return {before: beforeTs, after: afterTs};
}
private createSrcToOutPathMapper(outSrcMappings:
Array<{sourceFile: ts.SourceFile, outFileName: string}>):
(srcFileName: string) => string {
let srcToOutPath: (srcFileName: string) => string;
if (this.options.outDir) {
// TODO(tbosch): talk to TypeScript team to expose their logic for calculating the `rootDir`
// if none was specified.
if (outSrcMappings.length === 0) {
throw new Error(`Can't calculate the rootDir without at least one outSrcMapping. `);
}
const firstEntry = outSrcMappings[0];
const entrySrcDir = path.dirname(firstEntry.sourceFile.fileName);
const entryOutDir = path.dirname(firstEntry.outFileName);
const commonSuffix = longestCommonSuffix(entrySrcDir, entryOutDir);
const rootDir = entrySrcDir.substring(0, entrySrcDir.length - commonSuffix.length);
srcToOutPath = (srcFileName) =>
path.resolve(this.options.outDir, path.relative(rootDir, srcFileName));
} else {
srcToOutPath = (srcFileName) => srcFileName;
}
return srcToOutPath;
}
private initSync() {
if (this._analyzedModules) {
return;
}
const {tmpProgram, analyzedFiles, hostAdapter} = this._createProgramWithBasicStubs();
let analyzedModules: NgAnalyzedModules;
try {
analyzedModules = this._compiler.loadFilesSync(analyzedFiles);
} catch (e) {
analyzedModules = this.catchAnalysisError(e);
}
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter);
}
private _createProgramWithBasicStubs(): {
tmpProgram: ts.Program,
analyzedFiles: NgAnalyzedFile[],
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter
} {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initalized!`);
}
const analyzedFiles: NgAnalyzedFile[] = [];
const codegen = (fileName: string) => {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initalized!`);
}
const analyzedFile = this._compiler.analyzeFile(fileName);
analyzedFiles.push(analyzedFile);
const debug = fileName.endsWith('application_ref.ts');
return this._compiler.emitBasicStubs(analyzedFile);
};
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
this.rootNames, this.options, this.host, this.metadataCache, codegen);
const aotOptions = getAotCompilerOptions(this.options);
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
this._typeCheckHost = hostAdapter;
this._structuralDiagnostics = [];
const oldTsProgram = this.oldProgram ? this.oldProgram.getTsProgram() : undefined;
// Note: This is important to not produce a memory leak!
this.oldProgram = undefined;
const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram);
return {tmpProgram, analyzedFiles, hostAdapter};
}
private _updateProgramWithTypeCheckStubs(
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter) {
this._analyzedModules = analyzedModules;
const genFiles = this._compiler.emitTypeCheckStubs(analyzedModules);
genFiles.forEach(gf => hostAdapter.updateGeneratedFile(gf));
this._tsProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, tmpProgram);
// Note: the new ts program should be completely reusable by TypeScript as:
// - we cache all the files in the hostAdapter
// - new new stubs use the exactly same imports/exports as the old once (we assert that in
// hostAdapter.updateGeneratedFile).
if (tsStructureIsReused(tmpProgram) !== StructureIsReused.Completely) {
throw new Error(`Internal Error: The structure of the program changed during codegen.`);
}
}
private catchAnalysisError(e: any): NgAnalyzedModules {
if (isSyntaxError(e)) {
const parserErrors = getParseErrors(e);
@ -241,78 +353,32 @@ class AngularCompilerProgram implements Program {
code: DEFAULT_ERROR_CODE
}];
}
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.compiler.emitAllStubs(this.analyzedModules);
}
private generateFiles() {
const diags: Diagnostic[] = this._generatedFileDiagnostics = [];
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;
this._generatedFiles = this.compiler.emitAllImpls(this.analyzedModules);
} catch (e) {
this._generatedFiles = [];
if (isSyntaxError(e)) {
this._generatedFileDiagnostics = [{
diags.push({
messageText: e.message,
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}];
});
return [];
}
throw e;
}
}
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);
}
private generateSemanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
return translateDiagnostics(
this.programWithStubsHost, this.programWithStubs.getSemanticDiagnostics());
}
}
class AotCompilerHostImpl extends BaseAotCompilerHost<CompilerHost> {
moduleNameToFileName(m: string, containingFile: string): string|null {
return this.context.moduleNameToFileName(m, containingFile);
}
fileNameToModuleName(importedFile: string, containingFile: string): string|null {
return this.context.fileNameToModuleName(importedFile, containingFile);
}
resourceNameToFileName(resourceName: string, containingFile: string): string|null {
return this.context.resourceNameToFileName(resourceName, containingFile);
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return this.context.toSummaryFileName(fileName, referringSrcFileName);
}
fromSummaryFileName(fileName: string, referringLibFileName: string): string {
return this.context.fromSummaryFileName(fileName, referringLibFileName);
return translateDiagnostics(this.typeCheckHost, this.tsProgram.getSemanticDiagnostics());
}
}
@ -359,70 +425,20 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
};
}
function writeMetadata(
host: ts.CompilerHost, emitFilePath: string, sourceFile: ts.SourceFile,
metadataCache: LowerMetadataCache, onError?: (message: string) => void) {
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 = metadataCache.getMetadata(collectableFile);
if (metadata) {
const metadataText = JSON.stringify([metadata]);
host.writeFile(path, metadataText, false, onError, [sourceFile]);
}
}
}
function writeNgSummaryJson(
host: ts.CompilerHost, emitFilePath: string, sourceFile: ts.SourceFile,
generatedFilesByName: Map<string, GeneratedFile>, onError?: (message: string) => void) {
// Note: some files have an empty .ngfactory.js/.d.ts file but still need
// .ngsummary.json files (e.g. directives / pipes).
// We write the ngSummary when we try to emit the .ngfactory.js files
// and not the regular .js files as the latter are not emitted when
// we generate code for a npm library which ships .js / .d.ts / .metadata.json files.
if (/\.ngfactory.js$/.test(emitFilePath)) {
const emitPath = emitFilePath.replace(/\.ngfactory\.js$/, '.ngsummary.json');
const genFilePath = sourceFile.fileName.replace(/\.ngfactory\.ts$/, '.ngsummary.json');
const genFile = generatedFilesByName.get(genFilePath);
if (genFile) {
host.writeFile(emitPath, genFile.source !, false, onError, [sourceFile]);
}
}
}
function createWriteFileCallback(
emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache,
generatedFiles: GeneratedFile[]) {
const generatedFilesByName = new Map<string, GeneratedFile>();
generatedFiles.forEach(f => generatedFilesByName.set(f.genFileUrl, f));
options: {skipTemplateCodegen?: boolean}, host: ts.CompilerHost,
outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}>) {
return (fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
const isGenerated = GENERATED_FILES.test(fileName);
if (sourceFile) {
const isGenerated = GENERATED_FILES.test(fileName);
if (isGenerated) {
writeNgSummaryJson(host, fileName, sourceFile, generatedFilesByName, onError);
}
if (!isGenerated && (emitFlags & EmitFlags.Metadata)) {
writeMetadata(host, fileName, sourceFile, metadataCache, onError);
}
if (isGenerated) {
const genFile = generatedFilesByName.get(sourceFile.fileName);
if (!genFile || !genFile.stmts || !genFile.stmts.length) {
// Don't emit empty generated files
return;
}
}
outSrcMapping.push({outFileName: fileName, sourceFile});
}
if (isGenerated && options.skipTemplateCodegen) {
// Always generate the files if requested to ensure we capture any diagnostic errors but only
// don't emit them.
return;
}
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
};
@ -447,86 +463,12 @@ function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
return [];
}
function createProgramWithStubsHost(
generatedFiles: GeneratedFile[], originalProgram: ts.Program,
originalHost: ts.CompilerHost): ts.CompilerHost&TypeCheckHost {
interface FileData {
g: GeneratedFile;
s?: ts.SourceFile;
emitCtx?: EmitterVisitorContext;
function longestCommonSuffix(a: string, b: string): string {
let len = 0;
while (a.charCodeAt(a.length - 1 - len) === b.charCodeAt(b.length - 1 - len)) {
len++;
}
return new class implements ts.CompilerHost, TypeCheckHost {
private generatedFiles: Map<string, FileData>;
private emitter = new TypeScriptEmitter();
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);
}
}
ngSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null {
const data = this.generatedFiles.get(fileName);
if (data && data.emitCtx) {
return data.emitCtx.spanOf(line, character);
}
return null;
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
const data = this.generatedFiles.get(fileName);
if (data) {
if (!data.s) {
const {sourceText, context} = this.emitter.emitStatementsAndContext(
data.g.srcFileUrl, data.g.genFileUrl, data.g.stmts !,
/* preamble */ undefined, /* emitSourceMaps */ undefined,
/* referenceFilter */ undefined);
data.emitCtx = context;
data.s = ts.createSourceFile(fileName, sourceText, languageVersion);
}
return data.s;
}
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);
};
return a.substring(a.length - len);
}
export function i18nExtract(

View File

@ -6,4 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}
// Note: This is an internal property in TypeScript. Use it only for assertions and tests.
export function tsStructureIsReused(program: ts.Program): StructureIsReused {
return (program as any).structureIsReused;
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler';
import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler';
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import {CompilerHostContext} from 'compiler-cli';
import * as fs from 'fs';
@ -175,9 +175,10 @@ export class DiagnosticContext {
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
result = this._resolver = new CompileMetadataResolver(
config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(),
elementSchemaRegistry, directiveNormalizer, new Console(), staticSymbolCache,
this.reflector, (error, type) => this.collectError(error, type && type.filePath));
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
staticSymbolCache, this.reflector,
(error, type) => this.collectError(error, type && type.filePath));
}
return result;
}
@ -186,12 +187,9 @@ export class DiagnosticContext {
let analyzedModules = this._analyzedModules;
if (!analyzedModules) {
const analyzeHost = {isSourceFile(filePath: string) { return true; }};
const programSymbols = extractProgramSymbols(
this.staticSymbolResolver, this.program.getSourceFiles().map(sf => sf.fileName),
analyzeHost);
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
analyzedModules = this._analyzedModules =
analyzeNgModules(programSymbols, analyzeHost, this.staticSymbolResolver, this.resolver);
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
}
return analyzedModules;
}

View File

@ -111,7 +111,7 @@ export class MockCompilerHost implements ts.CompilerHost {
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (message: string) => void): ts.SourceFile {
const sourceText = this.context.readFile(fileName);
if (sourceText) {
if (sourceText != null) {
return ts.createSourceFile(fileName, sourceText, languageVersion);
} else {
return undefined !;

View File

@ -363,7 +363,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true
},
"include": ["src/**/*.ts"]
}`);
@ -379,7 +378,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true,
"skipTemplateCodegen": true
},
"compilerOptions": {
@ -410,7 +408,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true,
"skipTemplateCodegen": false
},
"compilerOptions": {
@ -826,8 +823,7 @@ describe('ngc transformer command-line', () => {
write('lib1/tsconfig-lib1.json', `{
"extends": "../tsconfig-base.json",
"angularCompilerOptions": {
"generateCodeForLibraries": false,
"enableSummariesForJit": true
"generateCodeForLibraries": false
},
"compilerOptions": {
"rootDir": ".",
@ -849,8 +845,7 @@ describe('ngc transformer command-line', () => {
write('lib2/tsconfig-lib2.json', `{
"extends": "../tsconfig-base.json",
"angularCompilerOptions": {
"generateCodeForLibraries": false,
"enableSummariesForJit": true
"generateCodeForLibraries": false
},
"compilerOptions": {
"rootDir": ".",

View File

@ -6,59 +6,78 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as compiler from '@angular/compiler';
import * as ts from 'typescript';
import {MetadataCollector} from '../../src/metadata/collector';
import {CompilerHost, CompilerOptions} from '../../src/transformers/api';
import {createCompilerHost} from '../../src/transformers/compiler_host';
import {TsCompilerAotCompilerTypeCheckHostAdapter, createCompilerHost} from '../../src/transformers/compiler_host';
import {Directory, Entry, MockAotContext, MockCompilerHost} from '../mocks';
const dummyModule = 'export let foo: any[];';
const aGeneratedFile = new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(1))]);
const aGeneratedFileText = `var x:any = 1;\n`;
describe('NgCompilerHost', () => {
function createHost(
{files = {}, options = {basePath: '/tmp'}}: {files?: Directory,
options?: CompilerOptions} = {}) {
let codeGenerator: jasmine.Spy;
beforeEach(() => { codeGenerator = jasmine.createSpy('codeGenerator').and.returnValue([]); });
function createNgHost({files = {}}: {files?: Directory} = {}): CompilerHost {
const context = new MockAotContext('/tmp/', files);
const tsHost = new MockCompilerHost(context);
return createCompilerHost({tsHost, options});
return new MockCompilerHost(context) as ts.CompilerHost;
}
function createHost({
files = {},
options = {
basePath: '/tmp',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
},
ngHost = createNgHost({files}),
}: {files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost} = {}) {
return new TsCompilerAotCompilerTypeCheckHostAdapter(
['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator);
}
describe('fileNameToModuleName', () => {
let ngHost: CompilerHost;
beforeEach(() => { ngHost = createHost(); });
let host: TsCompilerAotCompilerTypeCheckHostAdapter;
beforeEach(() => { host = createHost(); });
it('should use a package import when accessing a package from a source file', () => {
expect(ngHost.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
expect(host.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
.toBe('@angular/core');
});
it('should use a package import when accessing a package from another package', () => {
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/mod1/index.d.ts', '/tmp/node_modules/mod2/index.d.ts'))
.toBe('mod1/index');
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/@angular/core/index.d.ts',
'/tmp/node_modules/@angular/common/index.d.ts'))
.toBe('@angular/core/index');
});
it('should use a relative import when accessing a file in the same package', () => {
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/mod/a/child.d.ts', '/tmp/node_modules/mod/index.d.ts'))
.toBe('./a/child');
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/@angular/core/src/core.d.ts',
'/tmp/node_modules/@angular/core/index.d.ts'))
.toBe('./src/core');
});
it('should use a relative import when accessing a source file from a source file', () => {
expect(ngHost.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
expect(host.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
.toBe('./a/child');
});
it('should support multiple rootDirs when accessing a source file form a source file', () => {
const ngHostWithMultipleRoots = createHost({
const hostWithMultipleRoots = createHost({
options: {
basePath: '/tmp/',
rootDirs: [
@ -68,59 +87,202 @@ describe('NgCompilerHost', () => {
}
});
// both files are in the rootDirs
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
.toBe('./b');
// one file is not in the rootDirs
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
.toBe('../c/c');
});
it('should error if accessing a source file from a package', () => {
expect(
() => ngHost.fileNameToModuleName(
() => host.fileNameToModuleName(
'/tmp/src/a/child.ts', '/tmp/node_modules/@angular/core.d.ts'))
.toThrowError(
'Trying to import a source file from a node_modules package: import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
'Trying to import a source file from a node_modules package: ' +
'import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.fileNameToModuleName = () => 'someResult';
const host = createHost({ngHost});
expect(host.fileNameToModuleName('a', 'b')).toBe('someResult');
});
});
describe('moduleNameToFileName', () => {
it('should resolve a package import without a containing file', () => {
const ngHost = createHost(
{files: {'tmp': {'node_modules': {'@angular': {'core': {'index.d.ts': dummyModule}}}}}});
expect(ngHost.moduleNameToFileName('@angular/core'))
.toBe('/tmp/node_modules/@angular/core/index.d.ts');
it('should resolve an import using the containing file', () => {
const host = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
expect(host.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.d.ts');
});
it('should resolve an import using the containing file', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
expect(ngHost.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.d.ts');
it('should allow to skip the containg file for package imports', () => {
const host =
createHost({files: {'tmp': {'node_modules': {'@core': {'index.d.ts': dummyModule}}}}});
expect(host.moduleNameToFileName('@core/index')).toBe('/tmp/node_modules/@core/index.d.ts');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.moduleNameToFileName = () => 'someResult';
const host = createHost({ngHost});
expect(host.moduleNameToFileName('a', 'b')).toBe('someResult');
});
});
describe('resourceNameToFileName', () => {
it('should resolve a relative import', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.html');
expect(ngHost.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts'))
.toBe(null);
expect(host.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts')).toBe(null);
});
it('should resolve package paths as relative paths', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.html');
});
it('should resolve absolute paths as package paths', () => {
const ngHost = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('/a/child.html', ''))
const host = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('/a/child.html', ''))
.toBe('/tmp/node_modules/a/child.html');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.resourceNameToFileName = () => 'someResult';
const host = createHost({ngHost});
expect(host.resourceNameToFileName('a', 'b')).toBe('someResult');
});
});
describe('getSourceFile', () => {
it('should cache source files by name', () => {
const host = createHost({files: {'tmp': {'src': {'index.ts': ``}}}});
const sf1 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
const sf2 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf1).toBe(sf2);
});
it('should generate code when asking for the base name and add it as referencedFiles', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({
files: {
'tmp': {
'src': {
'index.ts': `
/// <reference path="main.ts"/>
`
}
}
}
});
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
// the codegen should have been cached
expect(codeGenerator).toHaveBeenCalledTimes(1);
});
it('should generate code when asking for the generated name first', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({
files: {
'tmp': {
'src': {
'index.ts': `
/// <reference path="main.ts"/>
`
}
}
}
});
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
// the codegen should have been cached
expect(codeGenerator).toHaveBeenCalledTimes(1);
});
it('should clear old generated references if the original host cached them', () => {
const ngHost = createNgHost();
const sfText = `
/// <reference path="main.ts"/>
`;
const sf = ts.createSourceFile('/tmp/src/index.ts', sfText, ts.ScriptTarget.Latest);
ngHost.getSourceFile = () => sf;
codeGenerator.and.returnValue(
[new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [])]);
const host1 = createHost({ngHost});
host1.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles.length).toBe(2);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
codeGenerator.and.returnValue([]);
const host2 = createHost({ngHost});
host2.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles.length).toBe(1);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
});
});
describe('updateSourceFile', () => {
it('should update source files', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
let genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
host.updateGeneratedFile(new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(2))]));
genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(`var x:any = 2;\n`);
});
it('should error if the imports changed', () => {
codeGenerator.and.returnValue(
[new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [
new compiler.DeclareVarStmt(
'x', new compiler.ExternalExpr(new compiler.ExternalReference('aModule', 'aName')))
])]);
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(
() => host.updateGeneratedFile(new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt(
'x', new compiler.ExternalExpr(
new compiler.ExternalReference('otherModule', 'aName')))])))
.toThrowError([
`Illegal State: external references changed in /tmp/src/index.ngfactory.ts.`,
`Old: aModule.`, `New: otherModule`
].join('\n'));
});
});
});

View File

@ -0,0 +1,294 @@
/**
* @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 * as ng from '@angular/compiler-cli';
import {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
function getNgRootDir() {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
return moduleFilename.substr(0, distIndex);
}
describe('ng program', () => {
let basePath: string;
let write: (fileName: string, content: string) => void;
let errorSpy: jasmine.Spy&((s: string) => void);
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
mockDirs.forEach(
(dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); });
}
function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions {
return {
basePath,
'experimentalDecorators': true,
'skipLibCheck': true,
'strict': true,
'types': [],
'outDir': path.resolve(basePath, 'built'),
'rootDir': basePath,
'baseUrl': basePath,
'declaration': true,
'target': ts.ScriptTarget.ES5,
'module': ts.ModuleKind.ES2015,
'moduleResolution': ts.ModuleResolutionKind.NodeJs,
'lib': [
path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts')
],
'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions,
};
}
function expectNoDiagnostics(options: ng.CompilerOptions, p: ng.Program) {
const diags: ng.Diagnostics =
[...p.getTsSemanticDiagnostics(), ...p.getNgSemanticDiagnostics()];
if (diags.length > 0) {
console.error('Diagnostics: ' + ng.formatDiagnostics(options, diags));
throw new Error('Expected no diagnostics.');
}
}
beforeEach(() => {
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
basePath = makeTempDir();
write = (fileName: string, content: string) => {
const dir = path.dirname(fileName);
if (dir != '.') {
const newDir = path.join(basePath, dir);
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
}
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
};
const ngRootDir = getNgRootDir();
const nodeModulesPath = path.resolve(basePath, 'node_modules');
fs.mkdirSync(nodeModulesPath);
fs.symlinkSync(
path.resolve(ngRootDir, 'dist', 'all', '@angular'),
path.resolve(nodeModulesPath, '@angular'));
fs.symlinkSync(
path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs'));
fs.symlinkSync(
path.resolve(ngRootDir, 'node_modules', 'typescript'),
path.resolve(nodeModulesPath, 'typescript'));
});
describe('reuse of old ts program', () => {
const files = {
'src/util.ts': `export const x = 1;`,
'src/main.ts': `
import {NgModule, Component} from '@angular/core';
import {x} from './util';
@Component({selector: 'comp', templateUrl: './main.html'})
export class MyComp {}
@NgModule()
export class MyModule {}
`,
'src/main.html': `Hello world`,
};
function expectResuse(newFiles: {[fileName: string]: string}, reuseLevel: StructureIsReused) {
writeFiles(files);
const options1 = createCompilerOptions();
const host1 = ng.createCompilerHost({options: options1});
const rootNames1 = [path.resolve(basePath, 'src/main.ts')];
const p1 = ng.createProgram({rootNames: rootNames1, options: options1, host: host1});
expectNoDiagnostics(options1, p1);
// Note: we recreate the options, rootNames and the host
// to check that TS checks against values, and not references!
writeFiles(newFiles);
const options2 = {...options1};
const host2 = ng.createCompilerHost({options: options2});
const rootNames2 = [...rootNames1];
const p2 =
ng.createProgram({rootNames: rootNames2, options: options2, host: host2, oldProgram: p1});
expectNoDiagnostics(options1, p2);
expect(tsStructureIsReused(p1.getTsProgram())).toBe(reuseLevel);
}
it('should reuse completely if nothing changed',
() => { expectResuse({}, StructureIsReused.Completely); });
it('should resuse if a template or a ts file changed', () => {
expectResuse(
{
'src/main.html': `Some other text`,
'src/util.ts': `export const x = 2;`,
},
StructureIsReused.Completely);
});
it('should not reuse if an import changed', () => {
expectResuse(
{
'src/util.ts': `
import {Injectable} from '@angular/core';
export const x = 2;
`,
},
StructureIsReused.SafeModules);
});
});
it('should typecheck templates even if skipTemplateCodegen is set', () => {
writeFiles({
'src/main.ts': `
import {NgModule, Component} from '@angular/core';
@Component({selector: 'mycomp', template: '{{nonExistent}}'})
export class MyComp {}
@NgModule({declarations: [MyComp]})
export class MyModule {}
`
});
const options = createCompilerOptions({skipTemplateCodegen: true});
const host = ng.createCompilerHost({options});
const program =
ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host});
const diags = program.getNgSemanticDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'MyComp'.`);
});
it('should be able to use asynchronously loaded resources', (done) => {
writeFiles({
'src/main.ts': `
import {NgModule, Component} from '@angular/core';
@Component({selector: 'mycomp', templateUrl: './main.html'})
export class MyComp {}
@NgModule({declarations: [MyComp]})
export class MyModule {}
`,
// Note: we need to be able to resolve the template synchronously,
// only the content is delivered asynchronously.
'src/main.html': '',
});
const options = createCompilerOptions();
const host = ng.createCompilerHost({options});
host.readResource = () => Promise.resolve('Hello world!');
const program =
ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host});
program.loadNgStructureAsync().then(() => {
program.emit();
const factory = fs.readFileSync(path.resolve(basePath, 'built/src/main.ngfactory.js'));
expect(factory).toContain('Hello world!');
done();
});
});
});
function appComponentSource(): string {
return `
import {Component, Pipe, Directive} from '@angular/core';
export interface Person {
name: string;
address: Address;
}
export interface Address {
street: string;
city: string;
state: string;
zip: string;
}
@Component({
templateUrl: './app.component.html'
})
export class AppComponent {
name = 'Angular';
person: Person;
people: Person[];
maybePerson?: Person;
getName(): string { return this.name; }
getPerson(): Person { return this.person; }
getMaybePerson(): Person | undefined { return this.maybePerson; }
}
@Pipe({
name: 'aPipe',
})
export class APipe {
transform(n: number): number { return n + 1; }
}
@Directive({
selector: '[aDir]',
exportAs: 'aDir'
})
export class ADirective {
name = 'ADirective';
}
`;
}
const QUICKSTART = {
'src/app.component.ts': appComponentSource(),
'src/app.component.html': '<h1>Hello {{name}}</h1>',
'src/app.module.ts': `
import { NgModule } from '@angular/core';
import { AppComponent, APipe, ADirective } from './app.component';
@NgModule({
declarations: [ AppComponent, APipe, ADirective ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
};
const LOWERING_QUICKSTART = {
'src/app.component.ts': appComponentSource(),
'src/app.component.html': '<h1>Hello {{name}}</h1>',
'src/app.module.ts': `
import { NgModule, Component } from '@angular/core';
import { AppComponent, APipe, ADirective } from './app.component';
class Foo {}
@Component({
template: '',
providers: [
{provide: 'someToken', useFactory: () => new Foo()}
]
})
export class Bar {}
@NgModule({
declarations: [ AppComponent, APipe, ADirective, Bar ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
};
function expectNoDiagnostics(diagnostics: ng.Diagnostics) {
if (diagnostics && diagnostics.length) {
throw new Error(ng.formatDiagnostics({}, diagnostics));
}
}