diff --git a/integration/ng-cli-create.sh b/integration/ng-cli-create.sh index 8177088680..cd62d0bfe5 100755 --- a/integration/ng-cli-create.sh +++ b/integration/ng-cli-create.sh @@ -38,8 +38,7 @@ else sed -i -E 's/ng build/ng build --prod --build-optimizer/g' package.json sed -i -E 's/ng test/ng test --single-run/g' package.json - # workaround for https://github.com/angular/angular-cli/issues/7401 - sed -i -E 's/"@angular\/cli\"\: \".*\"/"@angular\/cli": "https:\/\/github.com\/angular\/cli-builds"/g' package.json + sed -i -E 's/"typescript\"\: \".*\"/"typescript": "2.4.2"/g' package.json yarn add \ file:../../dist/packages-dist/compiler-cli \ diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 9c8f143e61..903be05092 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -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; } @@ -35,9 +33,7 @@ export abstract class BaseAotCompilerHost private flatModuleIndexNames = new Set(); private flatModuleIndexRedirectNames = new Set(); - 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 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 } } - 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 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 { + protected metadataProvider: MetadataCollector; protected basePath: string; private moduleFileNames = new Map(); private isGenDirChildOfRootDir: boolean; @@ -241,10 +227,10 @@ export class CompilerHost extends BaseAotCompilerHost { 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 { 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'; } diff --git a/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts b/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts index b15543cd0a..7f5b867bdb 100644 --- a/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts +++ b/packages/compiler-cli/src/diagnostics/translate_diagnostics.ts @@ -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 { diff --git a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts index 944216f84b..fd31380319 100644 --- a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts +++ b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts @@ -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'; diff --git a/packages/compiler-cli/src/language_services.ts b/packages/compiler-cli/src/language_services.ts index b5b27364d5..877a0f8283 100644 --- a/packages/compiler-cli/src/language_services.ts +++ b/packages/compiler-cli/src/language_services.ts @@ -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'; diff --git a/packages/compiler-cli/src/ngtools_api2.ts b/packages/compiler-cli/src/ngtools_api2.ts index 9327655286..7b6c36b536 100644 --- a/packages/compiler-cli/src/ngtools_api2.ts +++ b/packages/compiler-cli/src/ngtools_api2.ts @@ -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; } diff --git a/packages/compiler-cli/src/path_mapped_compiler_host.ts b/packages/compiler-cli/src/path_mapped_compiler_host.ts index 4df2e31740..848bb51886 100644 --- a/packages/compiler-cli/src/path_mapped_compiler_host.ts +++ b/packages/compiler-cli/src/path_mapped_compiler_host.ts @@ -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] : []; } } diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 67908cbc7f..e8ef4d13df 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -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 = []; diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index 1e64e89525..9f07f90a25 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -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}), diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 1f340efab7..cf7053207d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -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` 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, diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index e2ae268f25..0e3f3c2831 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -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; + 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 implements ts.CompilerHost, + TypeCheckHost { + private rootDirs: string[]; + private moduleResolutionCache: ts.ModuleResolutionCache; + private originalSourceFiles = new Map(); + private generatedSourceFiles = new Map(); + private generatedCodeFor = new Set(); + 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 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(); - 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): 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 { + 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\$.*/, ''); } diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 017afabe29..66ce9922a2 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -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 { - 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 { - 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, 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(); - 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; - 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( diff --git a/packages/compiler-cli/src/transformers/util.ts b/packages/compiler-cli/src/transformers/util.ts index 7e14366c6e..bd2b78b71c 100644 --- a/packages/compiler-cli/src/transformers/util.ts +++ b/packages/compiler-cli/src/transformers/util.ts @@ -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; +} \ No newline at end of file diff --git a/packages/compiler-cli/test/diagnostics/mocks.ts b/packages/compiler-cli/test/diagnostics/mocks.ts index 60d3bbf2db..1f7965b845 100644 --- a/packages/compiler-cli/test/diagnostics/mocks.ts +++ b/packages/compiler-cli/test/diagnostics/mocks.ts @@ -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; } diff --git a/packages/compiler-cli/test/mocks.ts b/packages/compiler-cli/test/mocks.ts index 76cea0cf19..88503a67ae 100644 --- a/packages/compiler-cli/test/mocks.ts +++ b/packages/compiler-cli/test/mocks.ts @@ -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 !; diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index c1fae5e477..667febfef3 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -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": ".", diff --git a/packages/compiler-cli/test/transformers/compiler_host_spec.ts b/packages/compiler-cli/test/transformers/compiler_host_spec.ts index 10f5450e07..5364dfa7ca 100644 --- a/packages/compiler-cli/test/transformers/compiler_host_spec.ts +++ b/packages/compiler-cli/test/transformers/compiler_host_spec.ts @@ -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': '
'}}}}}); - expect(ngHost.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts')) + const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '
'}}}}}); + 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': '
'}}}}}); - expect(ngHost.resourceNameToFileName('a/child.html', '/tmp/src/index.ts')) + const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '
'}}}}}); + 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': '
'}}}}}); - expect(ngHost.resourceNameToFileName('/a/child.html', '')) + const host = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '
'}}}}}); + 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': ` + /// + ` + } + } + } + }); + + 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': ` + /// + ` + } + } + } + }); + + 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 = ` + /// + `; + 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')); + }); }); }); diff --git a/packages/compiler-cli/test/transformers/program_spec.ts b/packages/compiler-cli/test/transformers/program_spec.ts new file mode 100644 index 0000000000..65c1fba161 --- /dev/null +++ b/packages/compiler-cli/test/transformers/program_spec.ts @@ -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': '

Hello {{name}}

', + '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': '

Hello {{name}}

', + '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)); + } +} diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index aecae17915..aa21381d06 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata'; import {CompilerConfig} from '../config'; +import {ViewEncapsulation} from '../core'; import {MessageBundle} from '../i18n/message_bundle'; import {Identifiers, createTokenForExternalReference} from '../identifiers'; import {CompileMetadataResolver} from '../metadata_resolver'; @@ -21,7 +22,7 @@ import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; import {TemplateAst} from '../template_parser/template_ast'; import {TemplateParser} from '../template_parser/template_parser'; -import {OutputContext, syntaxError} from '../util'; +import {OutputContext, ValueVisitor, syntaxError, visitValue} from '../util'; import {TypeCheckCompiler} from '../view_compiler/type_check_compiler'; import {ViewCompileResult, ViewCompiler} from '../view_compiler/view_compiler'; @@ -33,6 +34,12 @@ import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolv import {createForJitStub, serializeSummaries} from './summary_serializer'; import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util'; +export enum StubEmitFlags { + Basic = 1 << 0, + TypeCheck = 1 << 1, + All = TypeCheck | Basic +} + export class AotCompiler { private _templateAstCache = new Map(); @@ -40,20 +47,20 @@ export class AotCompiler { constructor( private _config: CompilerConfig, private _host: AotCompilerHost, private _reflector: StaticReflector, private _metadataResolver: CompileMetadataResolver, - private _htmlParser: HtmlParser, private _templateParser: TemplateParser, - private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, - private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler, - private _outputEmitter: OutputEmitter, + private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, + private _viewCompiler: ViewCompiler, private _typeCheckCompiler: TypeCheckCompiler, + private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter, private _summaryResolver: SummaryResolver, private _localeId: string|null, - private _translationFormat: string|null, private _enableSummariesForJit: boolean|null, - private _symbolResolver: StaticSymbolResolver) {} + private _translationFormat: string|null, + /** TODO(tbosch): remove this flag as it is always on in the new ngc */ + private _enableSummariesForJit: boolean|null, private _symbolResolver: StaticSymbolResolver) { + } clearCache() { this._metadataResolver.clearCache(); } analyzeModulesSync(rootFiles: string[]): NgAnalyzedModules { - const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); const analyzeResult = analyzeAndValidateNgModules( - programSymbols, this._host, this._symbolResolver, this._metadataResolver); + rootFiles, this._host, this._symbolResolver, this._metadataResolver); analyzeResult.ngModules.forEach( ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( ngModule.type.reference, true)); @@ -61,9 +68,8 @@ export class AotCompiler { } analyzeModulesAsync(rootFiles: string[]): Promise { - const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host); const analyzeResult = analyzeAndValidateNgModules( - programSymbols, this._host, this._symbolResolver, this._metadataResolver); + rootFiles, this._host, this._symbolResolver, this._metadataResolver); return Promise .all(analyzeResult.ngModules.map( ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( @@ -71,21 +77,183 @@ export class AotCompiler { .then(() => analyzeResult); } - emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] { - const {files, ngModuleByPipeOrDirective} = analyzeResult; - const sourceModules = files.map( - file => this._compileStubFile( - file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules)); - return flatten(sourceModules); + analyzeFile(fileName: string): NgAnalyzedFile { + return analyzeFile(this._host, this._symbolResolver, this._metadataResolver, fileName); } - emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] { - const {ngModuleByPipeOrDirective, files} = analyzeResult; - const sourceModules = files.map( - file => this._compileImplFile( - file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, - file.injectables)); - return flatten(sourceModules); + emitBasicStubs(file: NgAnalyzedFile): GeneratedFile[] { + return this._emitStubs(file, StubEmitFlags.Basic); + } + + emitTypeCheckStubs(files: NgAnalyzedModules): GeneratedFile[] { + const generatedFiles: GeneratedFile[] = []; + files.files.forEach( + file => this._emitStubs(file, StubEmitFlags.TypeCheck) + .forEach(genFile => generatedFiles.push(genFile))); + return generatedFiles; + } + + loadFilesAsync(files: NgAnalyzedFile[]): Promise { + const loadingPromises: Promise[] = []; + files.forEach( + file => file.ngModules.forEach( + ngModule => + loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( + ngModule.type.reference, false)))); + return Promise.all(loadingPromises).then(_ => mergeAndValidateNgFiles(files)); + } + + loadFilesSync(files: NgAnalyzedFile[]): NgAnalyzedModules { + files.forEach( + file => file.ngModules.forEach( + ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( + ngModule.type.reference, true))); + return mergeAndValidateNgFiles(files); + } + + private _emitStubs(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] { + return [ + ...this._createNgFactoryStub(file, emitFlags), + ...this._createExternalStyleSheetNgFactoryStubs(file, emitFlags), + ...this._createNgSummaryStub(file, emitFlags) + ]; + } + + private _createNgFactoryStub(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] { + const generatedFiles: GeneratedFile[] = []; + const outputCtx = this._createOutputContext(ngfactoryFilePath(file.fileName, true)); + + file.ngModules.forEach((ngModuleMeta, ngModuleIndex) => { + // Note: the code below needs to executed for StubEmitFlags.Basic and StubEmitFlags.TypeCheck, + // so we don't change the .ngfactory file too much when adding the typecheck block. + + // create exports that user code can reference + this._ngModuleCompiler.createStub(outputCtx, ngModuleMeta.type.reference); + + // add references to the symbols from the metadata. + // These can be used by the type check block for components, + // and they also cause TypeScript to include these files into the program too, + // which will make them part of the analyzedFiles. + const externalReferences: StaticSymbol[] = [ + ...ngModuleMeta.declaredDirectives.map(d => d.reference), + ...ngModuleMeta.declaredPipes.map(d => d.reference), + ...ngModuleMeta.importedModules.map(m => m.type.reference), + ...ngModuleMeta.exportedModules.map(m => m.type.reference), + ]; + const externalReferenceVars = new Map(); + externalReferences.forEach((ref, typeIndex) => { + if (this._host.isSourceFile(ref.filePath)) { + externalReferenceVars.set(ref, `_decl${ngModuleIndex}_${typeIndex}`); + } + }); + externalReferenceVars.forEach((varName, reference) => { + outputCtx.statements.push( + o.variable(varName) + .set(o.NULL_EXPR.cast(o.DYNAMIC_TYPE)) + .toDeclStmt(o.expressionType(outputCtx.importExpr(reference)))); + }); + + if (emitFlags & StubEmitFlags.TypeCheck) { + // add the typecheck block for all components of the NgModule + ngModuleMeta.declaredDirectives.forEach((dirId) => { + const compMeta = this._metadataResolver.getDirectiveMetadata(dirId.reference); + if (!compMeta.isComponent) { + return; + } + this._createTypeCheckBlock( + outputCtx, ngModuleMeta, this._metadataResolver.getHostComponentMetadata(compMeta), + [compMeta.type], externalReferenceVars); + this._createTypeCheckBlock( + outputCtx, ngModuleMeta, compMeta, ngModuleMeta.transitiveModule.directives, + externalReferenceVars); + }); + } + }); + + // make sure we create a .ngfactory if we have a least one component + // in the file. + // Only do this for StubEmitFlags.Basic, as adding a type check block + // does not change this file (as we generate type check blocks based on NgModules). + if (outputCtx.statements.length === 0 && (emitFlags & StubEmitFlags.Basic) && + file.directives.some( + dir => this._metadataResolver.getNonNormalizedDirectiveMetadata( + dir) !.metadata.isComponent)) { + _createEmptyStub(outputCtx); + } + + // make sure we create a .ngfactory if we reexport a non source file. + // Only do this for StubEmitFlags.Basic, as adding a type check block + // does not change this file (as we generate type check blocks based on NgModules). + if (outputCtx.statements.length === 0 && (emitFlags & StubEmitFlags.Basic) && + file.exportsNonSourceFiles) { + _createEmptyStub(outputCtx); + } + + if (outputCtx.statements.length > 0) { + generatedFiles.push(this._codegenSourceModule(file.fileName, outputCtx)); + } + return generatedFiles; + } + + private _createExternalStyleSheetNgFactoryStubs(file: NgAnalyzedFile, emitFlags: StubEmitFlags): + GeneratedFile[] { + const generatedFiles: GeneratedFile[] = []; + if (!(emitFlags & StubEmitFlags.Basic)) { + // note: stylesheet stubs don't change when we produce type check stubs + return generatedFiles; + } + const fileSuffix = splitTypescriptSuffix(file.fileName, true)[1]; + file.directives.forEach((dirSymbol) => { + const compMeta = + this._metadataResolver.getNonNormalizedDirectiveMetadata(dirSymbol) !.metadata; + if (!compMeta.isComponent) { + return; + } + // Note: compMeta is a component and therefore template is non null. + compMeta.template !.styleUrls.forEach((styleUrl) => { + const normalizedUrl = this._host.resourceNameToFileName(styleUrl, file.fileName); + if (!normalizedUrl) { + throw new Error(`Couldn't resolve resource ${styleUrl} relative to ${file.fileName}`); + } + const encapsulation = + compMeta.template !.encapsulation || this._config.defaultEncapsulation; + const outputCtx = this._createOutputContext(_stylesModuleUrl( + normalizedUrl, encapsulation === ViewEncapsulation.Emulated, fileSuffix)); + _createEmptyStub(outputCtx); + generatedFiles.push(this._codegenSourceModule(normalizedUrl, outputCtx)); + }); + }); + return generatedFiles; + } + + private _createNgSummaryStub(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] { + const generatedFiles: GeneratedFile[] = []; + // note: .ngsummary.js stubs don't change when we produce type check stubs + if (!this._enableSummariesForJit || !(emitFlags & StubEmitFlags.Basic)) { + return generatedFiles; + } + if (file.directives.length || file.injectables.length || file.ngModules.length || + file.pipes.length || file.exportsNonSourceFiles) { + const outputCtx = this._createOutputContext(summaryForJitFileName(file.fileName, true)); + file.ngModules.forEach(ngModule => { + // create exports that user code can reference + createForJitStub(outputCtx, ngModule.type.reference); + }); + if (outputCtx.statements.length === 0) { + _createEmptyStub(outputCtx); + } + generatedFiles.push(this._codegenSourceModule(file.fileName, outputCtx)); + } + return generatedFiles; + } + + private _createTypeCheckBlock( + ctx: OutputContext, moduleMeta: CompileNgModuleMetadata, compMeta: CompileDirectiveMetadata, + directives: CompileIdentifierMetadata[], externalReferenceVars: Map) { + const {template: parsedTemplate, pipes: usedPipes} = + this._parseTemplate(compMeta, moduleMeta, directives); + ctx.statements.push(...this._typeCheckCompiler.compileComponent( + compMeta, parsedTemplate, usedPipes, externalReferenceVars)); } emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle { @@ -107,7 +275,8 @@ export class AotCompiler { const html = compMeta.template !.template !; const interpolationConfig = InterpolationConfig.fromArray(compMeta.template !.interpolation); - errors.push(...messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig) !); + errors.push( + ...messageBundle.updateFromTemplate(html, file.fileName, interpolationConfig) !); }); }); @@ -118,69 +287,18 @@ export class AotCompiler { return messageBundle; } - private _compileStubFile( - srcFileUrl: string, ngModuleByPipeOrDirective: Map, - directives: StaticSymbol[], pipes: StaticSymbol[], - ngModules: StaticSymbol[]): GeneratedFile[] { - const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; - const generatedFiles: GeneratedFile[] = []; - - const ngFactoryOutputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true)); - const jitSummaryOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true)); - - // create exports that user code can reference - ngModules.forEach((ngModuleReference) => { - this._ngModuleCompiler.createStub(ngFactoryOutputCtx, ngModuleReference); - createForJitStub(jitSummaryOutputCtx, ngModuleReference); - }); - - // create stubs for external stylesheets (always empty, as users should not import anything from - // the generated code) - directives.forEach((dirType) => { - const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); - if (!compMeta.isComponent) { - return; - } - const ngModule = ngModuleByPipeOrDirective.get(dirType); - if (!ngModule) { - throw new Error( - `Internal Error: cannot determine the module for component ${identifierName(compMeta.type)}!`); - } - this._compileComponentTypeCheckBlock( - ngFactoryOutputCtx, compMeta, ngModule, ngModule.transitiveModule.directives); - - // Note: compMeta is a component and therefore template is non null. - compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { - const styleContext = this._createOutputContext(_stylesModuleUrl( - stylesheetMeta.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), fileSuffix)); - _createStub(styleContext); - generatedFiles.push(this._codegenSourceModule(stylesheetMeta.moduleUrl !, styleContext)); - }); - }); - - if (ngFactoryOutputCtx.statements.length <= 0) { - _createStub(ngFactoryOutputCtx); - } - if (jitSummaryOutputCtx.statements.length <= 0) { - _createStub(jitSummaryOutputCtx); - } - - // Note: we are creating stub ngfactory/ngsummary for all source files, - // as the real calculation requires almost the same logic as producing the real content for - // them. Our pipeline will filter out empty ones at the end. Because of this filter, however, - // stub references to the reference type needs to be generated even if the user cannot - // refer to type from the `.d.ts` file to prevent the file being elided from the emit. - generatedFiles.push(this._codegenSourceModule(srcFileUrl, ngFactoryOutputCtx)); - if (this._enableSummariesForJit) { - generatedFiles.push(this._codegenSourceModule(srcFileUrl, jitSummaryOutputCtx)); - } - - return generatedFiles; + emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] { + const {ngModuleByPipeOrDirective, files} = analyzeResult; + const sourceModules = files.map( + file => this._compileImplFile( + file.fileName, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules, + file.injectables)); + return flatten(sourceModules); } private _compileImplFile( srcFileUrl: string, ngModuleByPipeOrDirective: Map, - directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[], + directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[], injectables: StaticSymbol[]): GeneratedFile[] { const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; const generatedFiles: GeneratedFile[] = []; @@ -191,7 +309,7 @@ export class AotCompiler { ...this._createSummary(srcFileUrl, directives, pipes, ngModules, injectables, outputCtx)); // compile all ng modules - ngModules.forEach((ngModuleType) => this._compileModule(outputCtx, ngModuleType)); + ngModules.forEach((ngModuleMeta) => this._compileModule(outputCtx, ngModuleMeta)); // compile components directives.forEach((dirType) => { @@ -228,7 +346,7 @@ export class AotCompiler { private _createSummary( srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[], - ngModules: StaticSymbol[], injectables: StaticSymbol[], + ngModules: CompileNgModuleMetadata[], injectables: StaticSymbol[], ngFactoryCtx: OutputContext): GeneratedFile[] { const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName) .map(symbol => this._symbolResolver.resolveSymbol(symbol)); @@ -238,10 +356,11 @@ export class AotCompiler { CompileTypeMetadata }[] = [ - ...ngModules.map(ref => ({ - summary: this._metadataResolver.getNgModuleSummary(ref) !, - metadata: this._metadataResolver.getNgModuleMetadata(ref) ! - })), + ...ngModules.map( + meta => ({ + summary: this._metadataResolver.getNgModuleSummary(meta.type.reference) !, + metadata: this._metadataResolver.getNgModuleMetadata(meta.type.reference) ! + })), ...directives.map(ref => ({ summary: this._metadataResolver.getDirectiveSummary(ref) !, metadata: this._metadataResolver.getDirectiveMetadata(ref) ! @@ -273,8 +392,7 @@ export class AotCompiler { return [summaryJson]; } - private _compileModule(outputCtx: OutputContext, ngModuleType: StaticSymbol): void { - const ngModule = this._metadataResolver.getNgModuleMetadata(ngModuleType) !; + private _compileModule(outputCtx: OutputContext, ngModule: CompileNgModuleMetadata): void { const providers: CompileProviderMetadata[] = []; if (this._localeId) { @@ -298,10 +416,7 @@ export class AotCompiler { private _compileComponentFactory( outputCtx: OutputContext, compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, fileSuffix: string): void { - const hostType = this._metadataResolver.getHostComponentType(compMeta.type.reference); - const hostMeta = createHostComponentMeta( - hostType, compMeta, this._metadataResolver.getHostComponentViewClass(hostType), - this._htmlParser); + const hostMeta = this._metadataResolver.getHostComponentMetadata(compMeta); const hostViewFactoryVar = this._compileComponent(outputCtx, hostMeta, ngModule, [compMeta.type], null, fileSuffix) .viewClassVar; @@ -336,26 +451,6 @@ export class AotCompiler { [o.StmtModifier.Final, o.StmtModifier.Exported])); } - private _parseTemplate( - compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, - directiveIdentifiers: CompileIdentifierMetadata[]): - {template: TemplateAst[], pipes: CompilePipeSummary[]} { - let result = this._templateAstCache.get(compMeta.type.reference); - if (result) { - return result; - } - const preserveWhitespaces = compMeta !.template !.preserveWhitespaces; - const directives = - directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference)); - const pipes = ngModule.transitiveModule.pipes.map( - pipe => this._metadataResolver.getPipeSummary(pipe.reference)); - result = this._templateParser.parse( - compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas, - templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces); - this._templateAstCache.set(compMeta.type.reference, result); - return result; - } - private _compileComponent( outputCtx: OutputContext, compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[], @@ -373,12 +468,23 @@ export class AotCompiler { return viewResult; } - private _compileComponentTypeCheckBlock( - outputCtx: OutputContext, compMeta: CompileDirectiveMetadata, - ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[]) { - const {template: parsedTemplate, pipes: usedPipes} = - this._parseTemplate(compMeta, ngModule, directiveIdentifiers); - this._typeCheckCompiler.compileComponent(outputCtx, compMeta, parsedTemplate, usedPipes); + private _parseTemplate( + compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, + directiveIdentifiers: CompileIdentifierMetadata[]): + {template: TemplateAst[], pipes: CompilePipeSummary[]} { + if (this._templateAstCache.has(compMeta.type.reference)) { + return this._templateAstCache.get(compMeta.type.reference) !; + } + const preserveWhitespaces = compMeta !.template !.preserveWhitespaces; + const directives = + directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference)); + const pipes = ngModule.transitiveModule.pipes.map( + pipe => this._metadataResolver.getPipeSummary(pipe.reference)); + const result = this._templateParser.parse( + compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas, + templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces); + this._templateAstCache.set(compMeta.type.reference, result); + return result; } private _createOutputContext(genFilePath: string): OutputContext { @@ -433,13 +539,14 @@ export class AotCompiler { } } -function _createStub(outputCtx: OutputContext) { +function _createEmptyStub(outputCtx: OutputContext) { // Note: We need to produce at least one import statement so that // TypeScript knows that the file is an es6 module. Otherwise our generated // exports / imports won't be emitted properly by TypeScript. outputCtx.statements.push(o.importExpr(Identifiers.ComponentFactory).toStmt()); } + function _resolveStyleStatements( symbolResolver: StaticSymbolResolver, compileResult: CompiledStylesheet, needsShim: boolean, fileSuffix: string): void { @@ -456,180 +563,164 @@ function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string): export interface NgAnalyzedModules { ngModules: CompileNgModuleMetadata[]; ngModuleByPipeOrDirective: Map; - files: Array<{ - srcUrl: string, - directives: StaticSymbol[], - pipes: StaticSymbol[], - ngModules: StaticSymbol[], - injectables: StaticSymbol[] - }>; + files: NgAnalyzedFile[]; symbolsMissingModule?: StaticSymbol[]; } +export interface NgAnalyzedFile { + fileName: string; + directives: StaticSymbol[]; + pipes: StaticSymbol[]; + ngModules: CompileNgModuleMetadata[]; + injectables: StaticSymbol[]; + exportsNonSourceFiles: boolean; +} + export interface NgAnalyzeModulesHost { isSourceFile(filePath: string): boolean; } -// Returns all the source files and a mapping from modules to directives export function analyzeNgModules( - programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost, - staticSymbolResolver: StaticSymbolResolver, + fileNames: string[], host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver, metadataResolver: CompileMetadataResolver): NgAnalyzedModules { - const programStaticSymbolsWithDecorators = programStaticSymbols.filter( - symbol => !symbol.filePath.endsWith('.d.ts') || - staticSymbolResolver.hasDecorators(symbol.filePath)); - const {ngModules, symbolsMissingModule} = - _createNgModules(programStaticSymbolsWithDecorators, host, metadataResolver); - return _analyzeNgModules( - programStaticSymbols, programStaticSymbolsWithDecorators, ngModules, symbolsMissingModule, - metadataResolver); + const files = _analyzeFilesIncludingNonProgramFiles( + fileNames, host, staticSymbolResolver, metadataResolver); + return mergeAnalyzedFiles(files); } export function analyzeAndValidateNgModules( - programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost, - staticSymbolResolver: StaticSymbolResolver, + fileNames: string[], host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver, metadataResolver: CompileMetadataResolver): NgAnalyzedModules { - const result = - analyzeNgModules(programStaticSymbols, host, staticSymbolResolver, metadataResolver); - if (result.symbolsMissingModule && result.symbolsMissingModule.length) { - const messages = result.symbolsMissingModule.map( + return validateAnalyzedModules( + analyzeNgModules(fileNames, host, staticSymbolResolver, metadataResolver)); +} + +function validateAnalyzedModules(analyzedModules: NgAnalyzedModules): NgAnalyzedModules { + if (analyzedModules.symbolsMissingModule && analyzedModules.symbolsMissingModule.length) { + const messages = analyzedModules.symbolsMissingModule.map( s => `Cannot determine the module for class ${s.name} in ${s.filePath}! Add ${s.name} to the NgModule to fix it.`); throw syntaxError(messages.join('\n')); } - return result; + return analyzedModules; } -function _analyzeNgModules( - programSymbols: StaticSymbol[], programSymbolsWithDecorators: StaticSymbol[], - ngModuleMetas: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[], - metadataResolver: CompileMetadataResolver): NgAnalyzedModules { - const moduleMetasByRef = new Map(); - ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule)); - const ngModuleByPipeOrDirective = new Map(); - const ngModulesByFile = new Map(); - const ngDirectivesByFile = new Map(); - const ngPipesByFile = new Map(); - const ngInjectablesByFile = new Map(); - const filePaths = new Set(); +// Analyzes all of the program files, +// including files that are not part of the program +// but are referenced by an NgModule. +function _analyzeFilesIncludingNonProgramFiles( + fileNames: string[], host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver, + metadataResolver: CompileMetadataResolver): NgAnalyzedFile[] { + const seenFiles = new Set(); + const files: NgAnalyzedFile[] = []; - // Make sure we produce an analyzed file for each input file - programSymbols.forEach((symbol) => { - const filePath = symbol.filePath; - filePaths.add(filePath); - }); - programSymbolsWithDecorators.forEach((symbol) => { - if (metadataResolver.isInjectable(symbol)) { - const filePath = symbol.filePath; - ngInjectablesByFile.set(filePath, (ngInjectablesByFile.get(filePath) || []).concat(symbol)); + const visitFile = (fileName: string) => { + if (seenFiles.has(fileName) || !host.isSourceFile(fileName)) { + return false; } - }); - - // Looping over all modules to construct: - // - a map from file to modules `ngModulesByFile`, - // - a map from file to directives `ngDirectivesByFile`, - // - a map from file to pipes `ngPipesByFile`, - // - a map from directive/pipe to module `ngModuleByPipeOrDirective`. - ngModuleMetas.forEach((ngModuleMeta) => { - const srcFileUrl = ngModuleMeta.type.reference.filePath; - filePaths.add(srcFileUrl); - ngModulesByFile.set( - srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference)); - - ngModuleMeta.declaredDirectives.forEach((dirIdentifier) => { - const fileUrl = dirIdentifier.reference.filePath; - filePaths.add(fileUrl); - ngDirectivesByFile.set( - fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirIdentifier.reference)); - ngModuleByPipeOrDirective.set(dirIdentifier.reference, ngModuleMeta); + seenFiles.add(fileName); + const analyzedFile = analyzeFile(host, staticSymbolResolver, metadataResolver, fileName); + files.push(analyzedFile); + analyzedFile.ngModules.forEach(ngModule => { + ngModule.transitiveModule.modules.forEach(modMeta => visitFile(modMeta.reference.filePath)); }); - ngModuleMeta.declaredPipes.forEach((pipeIdentifier) => { - const fileUrl = pipeIdentifier.reference.filePath; - filePaths.add(fileUrl); - ngPipesByFile.set( - fileUrl, (ngPipesByFile.get(fileUrl) || []).concat(pipeIdentifier.reference)); - ngModuleByPipeOrDirective.set(pipeIdentifier.reference, ngModuleMeta); - }); - }); - - const files: { - srcUrl: string, - directives: StaticSymbol[], - pipes: StaticSymbol[], - ngModules: StaticSymbol[], - injectables: StaticSymbol[] - }[] = []; - - filePaths.forEach((srcUrl) => { - const directives = ngDirectivesByFile.get(srcUrl) || []; - const pipes = ngPipesByFile.get(srcUrl) || []; - const ngModules = ngModulesByFile.get(srcUrl) || []; - const injectables = ngInjectablesByFile.get(srcUrl) || []; - files.push({srcUrl, directives, pipes, ngModules, injectables}); - }); - - return { - // map directive/pipe to module - ngModuleByPipeOrDirective, - // list modules and directives for every source file - files, - ngModules: ngModuleMetas, symbolsMissingModule }; + fileNames.forEach((fileName) => visitFile(fileName)); + return files; } -export function extractProgramSymbols( - staticSymbolResolver: StaticSymbolResolver, files: string[], - host: NgAnalyzeModulesHost): StaticSymbol[] { - const staticSymbols: StaticSymbol[] = []; - files.filter(fileName => host.isSourceFile(fileName)).forEach(sourceFile => { - staticSymbolResolver.getSymbolsOf(sourceFile).forEach((symbol) => { +export function analyzeFile( + host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver, + metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFile { + const directives: StaticSymbol[] = []; + const pipes: StaticSymbol[] = []; + const injectables: StaticSymbol[] = []; + const ngModules: CompileNgModuleMetadata[] = []; + const hasDecorators = staticSymbolResolver.hasDecorators(fileName); + let exportsNonSourceFiles = false; + // Don't analyze .d.ts files that have no decorators as a shortcut + // to speed up the analysis. This prevents us from + // resolving the references in these files. + // Note: exportsNonSourceFiles is only needed when compiling with summaries, + // which is not the case when .d.ts files are treated as input files. + if (!fileName.endsWith('.d.ts') || hasDecorators) { + staticSymbolResolver.getSymbolsOf(fileName).forEach((symbol) => { const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol); const symbolMeta = resolvedSymbol.metadata; - if (symbolMeta) { - if (symbolMeta.__symbolic != 'error') { - // Ignore symbols that are only included to record error information. - staticSymbols.push(resolvedSymbol.symbol); + if (!symbolMeta || symbolMeta.__symbolic === 'error') { + return; + } + exportsNonSourceFiles = + exportsNonSourceFiles || isValueExportingNonSourceFile(host, symbolMeta); + if (symbolMeta.__symbolic === 'class') { + if (metadataResolver.isDirective(symbol)) { + directives.push(symbol); + } else if (metadataResolver.isPipe(symbol)) { + pipes.push(symbol); + } else if (metadataResolver.isInjectable(symbol)) { + injectables.push(symbol); + } else { + const ngModule = metadataResolver.getNgModuleMetadata(symbol, false); + if (ngModule) { + ngModules.push(ngModule); + } } } }); - }); - - return staticSymbols; -} - -// Load the NgModules and check -// that all directives / pipes that are present in the program -// are also declared by a module. -function _createNgModules( - programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost, - metadataResolver: CompileMetadataResolver): - {ngModules: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[]} { - const ngModules = new Map(); - const programPipesAndDirectives: StaticSymbol[] = []; - const ngModulePipesAndDirective = new Set(); - - const addNgModule = (staticSymbol: any) => { - if (ngModules.has(staticSymbol) || !host.isSourceFile(staticSymbol.filePath)) { - return false; - } - const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false); - if (ngModule) { - ngModules.set(ngModule.type.reference, ngModule); - ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference)); - ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference)); - // For every input module add the list of transitively included modules - ngModule.transitiveModule.modules.forEach(modMeta => addNgModule(modMeta.reference)); - } - return !!ngModule; + } + return { + fileName, directives, pipes, ngModules, injectables, exportsNonSourceFiles, }; - programStaticSymbols.forEach((staticSymbol) => { - if (!addNgModule(staticSymbol) && - (metadataResolver.isDirective(staticSymbol) || metadataResolver.isPipe(staticSymbol))) { - programPipesAndDirectives.push(staticSymbol); +} + +function isValueExportingNonSourceFile(host: NgAnalyzeModulesHost, metadata: any): boolean { + let exportsNonSourceFiles = false; + + class Visitor implements ValueVisitor { + visitArray(arr: any[], context: any): any { arr.forEach(v => visitValue(v, this, context)); } + visitStringMap(map: {[key: string]: any}, context: any): any { + Object.keys(map).forEach((key) => visitValue(map[key], this, context)); } + visitPrimitive(value: any, context: any): any {} + visitOther(value: any, context: any): any { + if (value instanceof StaticSymbol && !host.isSourceFile(value.filePath)) { + exportsNonSourceFiles = true; + } + } + } + + visitValue(metadata, new Visitor(), null); + return exportsNonSourceFiles; +} + +export function mergeAnalyzedFiles(analyzedFiles: NgAnalyzedFile[]): NgAnalyzedModules { + const allNgModules: CompileNgModuleMetadata[] = []; + const ngModuleByPipeOrDirective = new Map(); + const allPipesAndDirectives = new Set(); + + analyzedFiles.forEach(af => { + af.ngModules.forEach(ngModule => { + allNgModules.push(ngModule); + ngModule.declaredDirectives.forEach( + d => ngModuleByPipeOrDirective.set(d.reference, ngModule)); + ngModule.declaredPipes.forEach(p => ngModuleByPipeOrDirective.set(p.reference, ngModule)); + }); + af.directives.forEach(d => allPipesAndDirectives.add(d)); + af.pipes.forEach(p => allPipesAndDirectives.add(p)); }); - // Throw an error if any of the program pipe or directives is not declared by a module - const symbolsMissingModule = - programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s)); - - return {ngModules: Array.from(ngModules.values()), symbolsMissingModule}; + const symbolsMissingModule: StaticSymbol[] = []; + allPipesAndDirectives.forEach(ref => { + if (!ngModuleByPipeOrDirective.has(ref)) { + symbolsMissingModule.push(ref); + } + }); + return { + ngModules: allNgModules, + ngModuleByPipeOrDirective, + symbolsMissingModule, + files: analyzedFiles + }; +} + +function mergeAndValidateNgFiles(files: NgAnalyzedFile[]): NgAnalyzedModules { + return validateAnalyzedModules(mergeAnalyzedFiles(files)); } diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index 4e96744587..de8645a3cc 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -77,17 +77,16 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom const tmplParser = new TemplateParser( config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []); const resolver = new CompileMetadataResolver( - config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector), - new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer, - console, symbolCache, staticReflector); + config, htmlParser, new NgModuleResolver(staticReflector), + new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver, + elementSchemaRegistry, normalizer, console, symbolCache, staticReflector); // TODO(vicb): do not pass options.i18nFormat here const viewCompiler = new ViewCompiler(config, staticReflector, elementSchemaRegistry); const typeCheckCompiler = new TypeCheckCompiler(options, staticReflector); const compiler = new AotCompiler( - config, compilerHost, staticReflector, resolver, htmlParser, tmplParser, - new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler, - new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver, - options.locale || null, options.i18nFormat || null, options.enableSummariesForJit || null, - symbolResolver); + config, compilerHost, staticReflector, resolver, tmplParser, new StyleCompiler(urlResolver), + viewCompiler, typeCheckCompiler, new NgModuleCompiler(staticReflector), + new TypeScriptEmitter(), summaryResolver, options.locale || null, options.i18nFormat || null, + options.enableSummariesForJit || null, symbolResolver); return {compiler, reflector: staticReflector}; } diff --git a/packages/compiler/src/aot/static_symbol_resolver.ts b/packages/compiler/src/aot/static_symbol_resolver.ts index 20e3dcf3a7..ee04005d73 100644 --- a/packages/compiler/src/aot/static_symbol_resolver.ts +++ b/packages/compiler/src/aot/static_symbol_resolver.ts @@ -252,7 +252,7 @@ export class StaticSymbolResolver { /** * hasDecorators checks a file's metadata for the presense of decorators without evalutating the - * metada. + * metadata. * * @param filePath the absolute path to examine for decorators. * @returns true if any class in the file has a decorator. @@ -435,7 +435,8 @@ export class StaticSymbolResolver { ResolvedStaticSymbol { sourceSymbol.assertNoMembers(); targetSymbol.assertNoMembers(); - if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath)) { + if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) && + this.summaryResolver.isLibraryFile(targetSymbol.filePath)) { // This case is for an ng library importing symbols from a plain ts library // transitively. // Note: We rely on the fact that we discover symbols in the direction diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 32313b0fc7..2816b09f69 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -9,10 +9,7 @@ import {StaticSymbol} from './aot/static_symbol'; import {ChangeDetectionStrategy, SchemaMetadata, Type, ViewEncapsulation} from './core'; import {LifecycleHooks} from './lifecycle_reflector'; -import * as html from './ml_parser/ast'; -import {HtmlParser} from './ml_parser/html_parser'; import {ParseTreeResult as HtmlParseTreeResult} from './ml_parser/parser'; -import {CssSelector} from './selector'; import {splitAtColon, stringify} from './util'; @@ -505,51 +502,6 @@ export class CompileDirectiveMetadata { } } -/** - * Construct {@link CompileDirectiveMetadata} from {@link ComponentTypeMetadata} and a selector. - */ -export function createHostComponentMeta( - hostTypeReference: any, compMeta: CompileDirectiveMetadata, - hostViewType: StaticSymbol | ProxyClass, htmlParser: HtmlParser): CompileDirectiveMetadata { - const template = CssSelector.parse(compMeta.selector !)[0].getMatchingElementTemplate(); - const templateUrl = ''; - const htmlAst = htmlParser.parse(template, templateUrl); - return CompileDirectiveMetadata.create({ - isHost: true, - type: {reference: hostTypeReference, diDeps: [], lifecycleHooks: []}, - template: new CompileTemplateMetadata({ - encapsulation: ViewEncapsulation.None, - template, - templateUrl, - htmlAst, - styles: [], - styleUrls: [], - ngContentSelectors: [], - animations: [], - isInline: true, - externalStylesheets: [], - interpolation: null, - preserveWhitespaces: false, - }), - exportAs: null, - changeDetection: ChangeDetectionStrategy.Default, - inputs: [], - outputs: [], - host: {}, - isComponent: true, - selector: '*', - providers: [], - viewProviders: [], - queries: [], - viewQueries: [], - componentViewType: hostViewType, - rendererType: - {id: '__Host__', encapsulation: ViewEncapsulation.None, styles: [], data: {}} as object, - entryComponents: [], - componentFactory: null - }); -} - export interface CompilePipeSummary extends CompileTypeSummary { type: CompileTypeMetadata; name: string; diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 8b42c8efc1..15c8a43aec 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -63,7 +63,7 @@ export * from './ml_parser/html_tags'; export * from './ml_parser/interpolation_config'; export * from './ml_parser/tags'; export {NgModuleCompiler} from './ng_module_compiler'; -export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement} from './output/output_ast'; +export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast'; export {EmitterVisitorContext} from './output/abstract_emitter'; export * from './output/ts_emitter'; export * from './parse_util'; diff --git a/packages/compiler/src/directive_normalizer.ts b/packages/compiler/src/directive_normalizer.ts index 445b4f4d52..2502bb5d44 100644 --- a/packages/compiler/src/directive_normalizer.ts +++ b/packages/compiler/src/directive_normalizer.ts @@ -12,6 +12,7 @@ import {ViewEncapsulation} from './core'; import * as html from './ml_parser/ast'; import {HtmlParser} from './ml_parser/html_parser'; import {InterpolationConfig} from './ml_parser/interpolation_config'; +import {ParseTreeResult as HtmlParseTreeResult} from './ml_parser/parser'; import {ResourceLoader} from './resource_loader'; import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver'; import {PreparsedElementType, preparseElement} from './template_parser/template_preparser'; @@ -88,12 +89,12 @@ export class DirectiveNormalizer { } return SyncAsync.then( - this.normalizeTemplateOnly(prenormData), - (result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result)); + this._preParseTemplate(prenormData), + (preparsedTemplate) => this._normalizeTemplateMetadata(prenormData, preparsedTemplate)); } - normalizeTemplateOnly(prenomData: PrenormalizedTemplateMetadata): - SyncAsync { + private _preParseTemplate(prenomData: PrenormalizedTemplateMetadata): + SyncAsync { let template: SyncAsync; let templateUrl: string; if (prenomData.template != null) { @@ -104,12 +105,12 @@ export class DirectiveNormalizer { template = this._fetch(templateUrl); } return SyncAsync.then( - template, (template) => this.normalizeLoadedTemplate(prenomData, template, templateUrl)); + template, (template) => this._preparseLoadedTemplate(prenomData, template, templateUrl)); } - normalizeLoadedTemplate( + private _preparseLoadedTemplate( prenormData: PrenormalizedTemplateMetadata, template: string, - templateAbsUrl: string): CompileTemplateMetadata { + templateAbsUrl: string): PreparsedTemplate { const isInline = !!prenormData.template; const interpolationConfig = InterpolationConfig.fromArray(prenormData.interpolation !); const rootNodesAndErrors = this._htmlParser.parse( @@ -123,69 +124,98 @@ export class DirectiveNormalizer { throw syntaxError(`Template parse errors:\n${errorString}`); } - const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({ - styles: prenormData.styles, - styleUrls: prenormData.styleUrls, - moduleUrl: prenormData.moduleUrl - })); + const templateMetadataStyles = this._normalizeStylesheet(new CompileStylesheetMetadata( + {styles: prenormData.styles, moduleUrl: prenormData.moduleUrl})); const visitor = new TemplatePreparseVisitor(); html.visitAll(visitor, rootNodesAndErrors.rootNodes); - const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata( + const templateStyles = this._normalizeStylesheet(new CompileStylesheetMetadata( {styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl})); + const styles = templateMetadataStyles.styles.concat(templateStyles.styles); + + const inlineStyleUrls = templateMetadataStyles.styleUrls.concat(templateStyles.styleUrls); + const styleUrls = this + ._normalizeStylesheet(new CompileStylesheetMetadata( + {styleUrls: prenormData.styleUrls, moduleUrl: prenormData.moduleUrl})) + .styleUrls; + return { + template, + templateUrl: templateAbsUrl, isInline, + htmlAst: rootNodesAndErrors, styles, inlineStyleUrls, styleUrls, + ngContentSelectors: visitor.ngContentSelectors, + }; + } + + private _normalizeTemplateMetadata( + prenormData: PrenormalizedTemplateMetadata, + preparsedTemplate: PreparsedTemplate): SyncAsync { + return SyncAsync.then( + this._loadMissingExternalStylesheets( + preparsedTemplate.styleUrls.concat(preparsedTemplate.inlineStyleUrls)), + (externalStylesheets) => this._normalizeLoadedTemplateMetadata( + prenormData, preparsedTemplate, externalStylesheets)); + } + + private _normalizeLoadedTemplateMetadata( + prenormData: PrenormalizedTemplateMetadata, preparsedTemplate: PreparsedTemplate, + stylesheets: Map): CompileTemplateMetadata { + // Algorithm: + // - produce exactly 1 entry per original styleUrl in + // CompileTemplateMetadata.externalStylesheets whith all styles inlined + // - inline all styles that are referenced by the template into CompileTemplateMetadata.styles. + // Reason: be able to determine how many stylesheets there are even without loading + // the template nor the stylesheets, so we can create a stub for TypeScript always synchronously + // (as resouce loading may be async) + + const styles = [...preparsedTemplate.styles]; + this._inlineStyles(preparsedTemplate.inlineStyleUrls, stylesheets, styles); + const styleUrls = preparsedTemplate.styleUrls; + + const externalStylesheets = styleUrls.map(styleUrl => { + const stylesheet = stylesheets.get(styleUrl) !; + const styles = [...stylesheet.styles]; + this._inlineStyles(stylesheet.styleUrls, stylesheets, styles); + return new CompileStylesheetMetadata({moduleUrl: styleUrl, styles: styles}); + }); + let encapsulation = prenormData.encapsulation; if (encapsulation == null) { encapsulation = this._config.defaultEncapsulation; } - - const styles = templateMetadataStyles.styles.concat(templateStyles.styles); - const styleUrls = templateMetadataStyles.styleUrls.concat(templateStyles.styleUrls); - if (encapsulation === ViewEncapsulation.Emulated && styles.length === 0 && styleUrls.length === 0) { encapsulation = ViewEncapsulation.None; } - return new CompileTemplateMetadata({ encapsulation, - template, - templateUrl: templateAbsUrl, - htmlAst: rootNodesAndErrors, styles, styleUrls, - ngContentSelectors: visitor.ngContentSelectors, + template: preparsedTemplate.template, + templateUrl: preparsedTemplate.templateUrl, + htmlAst: preparsedTemplate.htmlAst, styles, styleUrls, + ngContentSelectors: preparsedTemplate.ngContentSelectors, animations: prenormData.animations, - interpolation: prenormData.interpolation, isInline, - externalStylesheets: [], + interpolation: prenormData.interpolation, + isInline: preparsedTemplate.isInline, externalStylesheets, preserveWhitespaces: preserveWhitespacesDefault( prenormData.preserveWhitespaces, this._config.preserveWhitespaces), }); } - normalizeExternalStylesheets(templateMeta: CompileTemplateMetadata): - SyncAsync { - return SyncAsync.then( - this._loadMissingExternalStylesheets(templateMeta.styleUrls), - (externalStylesheets) => new CompileTemplateMetadata({ - encapsulation: templateMeta.encapsulation, - template: templateMeta.template, - templateUrl: templateMeta.templateUrl, - htmlAst: templateMeta.htmlAst, - styles: templateMeta.styles, - styleUrls: templateMeta.styleUrls, - externalStylesheets: externalStylesheets, - ngContentSelectors: templateMeta.ngContentSelectors, - animations: templateMeta.animations, - interpolation: templateMeta.interpolation, - isInline: templateMeta.isInline, - preserveWhitespaces: templateMeta.preserveWhitespaces, - })); + private _inlineStyles( + styleUrls: string[], stylesheets: Map, + targetStyles: string[]) { + styleUrls.forEach(styleUrl => { + const stylesheet = stylesheets.get(styleUrl) !; + stylesheet.styles.forEach(style => targetStyles.push(style)); + this._inlineStyles(stylesheet.styleUrls, stylesheets, targetStyles); + }); } private _loadMissingExternalStylesheets( styleUrls: string[], loadedStylesheets: Map = new Map()): - SyncAsync { + SyncAsync> { return SyncAsync.then( SyncAsync.all(styleUrls.filter((styleUrl) => !loadedStylesheets.has(styleUrl)) .map( @@ -193,16 +223,16 @@ export class DirectiveNormalizer { this._fetch(styleUrl), (loadedStyle) => { const stylesheet = - this.normalizeStylesheet(new CompileStylesheetMetadata( + this._normalizeStylesheet(new CompileStylesheetMetadata( {styles: [loadedStyle], moduleUrl: styleUrl})); loadedStylesheets.set(styleUrl, stylesheet); return this._loadMissingExternalStylesheets( stylesheet.styleUrls, loadedStylesheets); }))), - (_) => Array.from(loadedStylesheets.values())); + (_) => loadedStylesheets); } - normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata { + private _normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata { const moduleUrl = stylesheet.moduleUrl !; const allStyleUrls = stylesheet.styleUrls.filter(isStyleUrlResolvable) .map(url => this._urlResolver.resolve(moduleUrl, url)); @@ -218,6 +248,17 @@ export class DirectiveNormalizer { } } +interface PreparsedTemplate { + template: string; + templateUrl: string; + isInline: boolean; + htmlAst: HtmlParseTreeResult; + styles: string[]; + inlineStyleUrls: string[]; + styleUrls: string[]; + ngContentSelectors: string[]; +} + class TemplatePreparseVisitor implements html.Visitor { ngContentSelectors: string[] = []; styles: string[] = []; diff --git a/packages/compiler/src/i18n/extractor.ts b/packages/compiler/src/i18n/extractor.ts index 064347a120..80015d3fa0 100644 --- a/packages/compiler/src/i18n/extractor.ts +++ b/packages/compiler/src/i18n/extractor.ts @@ -10,7 +10,7 @@ /** * Extract i18n messages from source code */ -import {analyzeAndValidateNgModules, extractProgramSymbols} from '../aot/compiler'; +import {analyzeAndValidateNgModules} from '../aot/compiler'; import {createAotUrlResolver} from '../aot/compiler_factory'; import {StaticReflector} from '../aot/static_reflector'; import {StaticSymbolCache} from '../aot/static_symbol'; @@ -56,9 +56,8 @@ export class Extractor { private messageBundle: MessageBundle, private metadataResolver: CompileMetadataResolver) {} extract(rootFiles: string[]): Promise { - const programSymbols = extractProgramSymbols(this.staticSymbolResolver, rootFiles, this.host); const {files, ngModules} = analyzeAndValidateNgModules( - programSymbols, this.host, this.staticSymbolResolver, this.metadataResolver); + rootFiles, this.host, this.staticSymbolResolver, this.metadataResolver); return Promise .all(ngModules.map( ngModule => this.metadataResolver.loadNgModuleDirectiveAndPipeMetadata( @@ -79,7 +78,7 @@ export class Extractor { const interpolationConfig = InterpolationConfig.fromArray(compMeta.template !.interpolation); errors.push(...this.messageBundle.updateFromTemplate( - html, file.srcUrl, interpolationConfig) !); + html, file.fileName, interpolationConfig) !); }); }); @@ -108,9 +107,9 @@ export class Extractor { {get: (url: string) => host.loadResource(url)}, urlResolver, htmlParser, config); const elementSchemaRegistry = new DomElementSchemaRegistry(); const resolver = new CompileMetadataResolver( - config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector), - new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer, - console, symbolCache, staticReflector); + config, htmlParser, new NgModuleResolver(staticReflector), + new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver, + elementSchemaRegistry, normalizer, console, symbolCache, staticReflector); // TODO(vicb): implicit tags & attributes const messageBundle = new MessageBundle(htmlParser, [], {}, locale); diff --git a/packages/compiler/src/jit/compiler.ts b/packages/compiler/src/jit/compiler.ts index 5b913712dd..0bfac0db6b 100644 --- a/packages/compiler/src/jit/compiler.ts +++ b/packages/compiler/src/jit/compiler.ts @@ -6,18 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, CompileStylesheetMetadata, CompileTypeSummary, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileTypeSummary, ProviderMeta, ProxyClass, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; import {CompilerConfig} from '../config'; import {Type} from '../core'; import {CompileMetadataResolver} from '../metadata_resolver'; -import {HtmlParser} from '../ml_parser/html_parser'; import {NgModuleCompiler} from '../ng_module_compiler'; import * as ir from '../output/output_ast'; import {interpretStatements} from '../output/output_interpreter'; import {jitStatements} from '../output/output_jit'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; +import {TemplateAst} from '../template_parser/template_ast'; import {TemplateParser} from '../template_parser/template_parser'; import {Console, OutputContext, SyncAsync, stringify} from '../util'; import {ViewCompiler} from '../view_compiler/view_compiler'; @@ -44,11 +44,11 @@ export class JitCompiler { private _sharedStylesheetCount = 0; constructor( - private _metadataResolver: CompileMetadataResolver, private _htmlParser: HtmlParser, - private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, - private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler, - private _summaryResolver: SummaryResolver, private _reflector: CompileReflector, - private _compilerConfig: CompilerConfig, private _console: Console, + private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, + private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, + private _ngModuleCompiler: NgModuleCompiler, private _summaryResolver: SummaryResolver, + private _reflector: CompileReflector, private _compilerConfig: CompilerConfig, + private _console: Console, private getExtraNgModuleProviders: (ngModule: any) => CompileProviderMetadata[]) {} compileModuleSync(moduleType: Type): object { @@ -227,9 +227,8 @@ export class JitCompiler { const compMeta = this._metadataResolver.getDirectiveMetadata(compType); assertComponent(compMeta); - const hostClass = this._metadataResolver.getHostComponentType(compType); - const hostMeta = createHostComponentMeta( - hostClass, compMeta, (compMeta.componentFactory as any).viewDefFactory, this._htmlParser); + const hostMeta = this._metadataResolver.getHostComponentMetadata( + compMeta, (compMeta.componentFactory as any).viewDefFactory); compiledTemplate = new CompiledTemplate(true, compMeta.type, hostMeta, ngModule, [compMeta.type]); this._compiledHostTemplateCache.set(compType, compiledTemplate); @@ -257,21 +256,16 @@ export class JitCompiler { const externalStylesheetsByModuleUrl = new Map(); const outputContext = createOutputContext(); const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta); - const preserveWhitespaces = compMeta !.template !.preserveWhitespaces; compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { const compiledStylesheet = this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta); externalStylesheetsByModuleUrl.set(stylesheetMeta.moduleUrl !, compiledStylesheet); }); this._resolveStylesCompileResult(componentStylesheet, externalStylesheetsByModuleUrl); - const directives = - template.directives.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference)); const pipes = template.ngModule.transitiveModule.pipes.map( pipe => this._metadataResolver.getPipeSummary(pipe.reference)); - const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( - compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas, - templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !), - preserveWhitespaces); + const {template: parsedTemplate, pipes: usedPipes} = + this._parseTemplate(compMeta, template.ngModule, template.directives); const compileResult = this._viewCompiler.compileComponent( outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar), usedPipes); @@ -282,6 +276,21 @@ export class JitCompiler { template.compiled(viewClass, rendererType); } + private _parseTemplate( + compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, + directiveIdentifiers: CompileIdentifierMetadata[]): + {template: TemplateAst[], pipes: CompilePipeSummary[]} { + // Note: ! is ok here as components always have a template. + const preserveWhitespaces = compMeta.template !.preserveWhitespaces; + const directives = + directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference)); + const pipes = ngModule.transitiveModule.pipes.map( + pipe => this._metadataResolver.getPipeSummary(pipe.reference)); + return this._templateParser.parse( + compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas, + templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces); + } + private _resolveStylesCompileResult( result: CompiledStylesheet, externalStylesheetsByModuleUrl: Map) { result.dependencies.forEach((dep, i) => { diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index 334a20332d..c30b84a132 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -12,14 +12,16 @@ import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions'; import * as cpl from './compile_metadata'; import {CompileReflector} from './compile_reflector'; import {CompilerConfig} from './config'; -import {ChangeDetectionStrategy, Component, Directive, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core'; +import {ChangeDetectionStrategy, Component, Directive, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, ViewEncapsulation, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core'; import {DirectiveNormalizer} from './directive_normalizer'; import {DirectiveResolver} from './directive_resolver'; import {Identifiers} from './identifiers'; import {getAllLifecycleHooks} from './lifecycle_reflector'; +import {HtmlParser} from './ml_parser/html_parser'; import {NgModuleResolver} from './ng_module_resolver'; import {PipeResolver} from './pipe_resolver'; import {ElementSchemaRegistry} from './schema/element_schema_registry'; +import {CssSelector} from './selector'; import {SummaryResolver} from './summary_resolver'; import {Console, SyncAsync, ValueTransformer, isPromise, noUndefined, resolveForwardRef, stringify, syntaxError, visitValue} from './util'; @@ -44,9 +46,9 @@ export class CompileMetadataResolver { private _ngModuleOfTypes = new Map(); constructor( - private _config: CompilerConfig, private _ngModuleResolver: NgModuleResolver, - private _directiveResolver: DirectiveResolver, private _pipeResolver: PipeResolver, - private _summaryResolver: SummaryResolver, + private _config: CompilerConfig, private _htmlParser: HtmlParser, + private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver, + private _pipeResolver: PipeResolver, private _summaryResolver: SummaryResolver, private _schemaRegistry: ElementSchemaRegistry, private _directiveNormalizer: DirectiveNormalizer, private _console: Console, private _staticSymbolCache: StaticSymbolCache, private _reflector: CompileReflector, @@ -167,6 +169,54 @@ export class CompileMetadataResolver { return typeSummary && typeSummary.summaryKind === kind ? typeSummary : null; } + getHostComponentMetadata( + compMeta: cpl.CompileDirectiveMetadata, + hostViewType?: StaticSymbol|cpl.ProxyClass): cpl.CompileDirectiveMetadata { + const hostType = this.getHostComponentType(compMeta.type.reference); + if (!hostViewType) { + hostViewType = this.getHostComponentViewClass(hostType); + } + // Note: ! is ok here as this method should only be called with normalized directive + // metadata, which always fills in the selector. + const template = CssSelector.parse(compMeta.selector !)[0].getMatchingElementTemplate(); + const templateUrl = ''; + const htmlAst = this._htmlParser.parse(template, templateUrl); + return cpl.CompileDirectiveMetadata.create({ + isHost: true, + type: {reference: hostType, diDeps: [], lifecycleHooks: []}, + template: new cpl.CompileTemplateMetadata({ + encapsulation: ViewEncapsulation.None, + template, + templateUrl, + htmlAst, + styles: [], + styleUrls: [], + ngContentSelectors: [], + animations: [], + isInline: true, + externalStylesheets: [], + interpolation: null, + preserveWhitespaces: false, + }), + exportAs: null, + changeDetection: ChangeDetectionStrategy.Default, + inputs: [], + outputs: [], + host: {}, + isComponent: true, + selector: '*', + providers: [], + viewProviders: [], + queries: [], + viewQueries: [], + componentViewType: hostViewType, + rendererType: + {id: '__Host__', encapsulation: ViewEncapsulation.None, styles: [], data: {}} as object, + entryComponents: [], + componentFactory: null + }); + } + loadDirectiveMetadata(ngModuleType: any, directiveType: any, isSync: boolean): SyncAsync { if (this._directiveCache.has(directiveType)) { return null; diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 712f046d8b..afa3567f19 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -924,85 +924,109 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor { export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor { - visitReadVarExpr(ast: ReadVarExpr, context: any): any { return ast; } - visitWriteVarExpr(expr: WriteVarExpr, context: any): any { - expr.value.visitExpression(this, context); - return expr; + visitType(ast: Type, context: any): any { return ast; } + visitExpression(ast: Expression, context: any): any { + if (ast.type) { + ast.type.visitType(this, context); + } + return ast; } - visitWriteKeyExpr(expr: WriteKeyExpr, context: any): any { - expr.receiver.visitExpression(this, context); - expr.index.visitExpression(this, context); - expr.value.visitExpression(this, context); - return expr; + visitBuiltintType(type: BuiltinType, context: any): any { return this.visitType(type, context); } + visitExpressionType(type: ExpressionType, context: any): any { + type.value.visitExpression(this, context); + return this.visitType(type, context); } - visitWritePropExpr(expr: WritePropExpr, context: any): any { - expr.receiver.visitExpression(this, context); - expr.value.visitExpression(this, context); - return expr; + visitArrayType(type: ArrayType, context: any): any { return this.visitType(type, context); } + visitMapType(type: MapType, context: any): any { return this.visitType(type, context); } + visitReadVarExpr(ast: ReadVarExpr, context: any): any { + return this.visitExpression(ast, context); + } + visitWriteVarExpr(ast: WriteVarExpr, context: any): any { + ast.value.visitExpression(this, context); + return this.visitExpression(ast, context); + } + visitWriteKeyExpr(ast: WriteKeyExpr, context: any): any { + ast.receiver.visitExpression(this, context); + ast.index.visitExpression(this, context); + ast.value.visitExpression(this, context); + return this.visitExpression(ast, context); + } + visitWritePropExpr(ast: WritePropExpr, context: any): any { + ast.receiver.visitExpression(this, context); + ast.value.visitExpression(this, context); + return this.visitExpression(ast, context); } visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any { ast.receiver.visitExpression(this, context); this.visitAllExpressions(ast.args, context); - return ast; + return this.visitExpression(ast, context); } visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any { ast.fn.visitExpression(this, context); this.visitAllExpressions(ast.args, context); - return ast; + return this.visitExpression(ast, context); } visitInstantiateExpr(ast: InstantiateExpr, context: any): any { ast.classExpr.visitExpression(this, context); this.visitAllExpressions(ast.args, context); - return ast; + return this.visitExpression(ast, context); + } + visitLiteralExpr(ast: LiteralExpr, context: any): any { + return this.visitExpression(ast, context); + } + visitExternalExpr(ast: ExternalExpr, context: any): any { + if (ast.typeParams) { + ast.typeParams.forEach(type => type.visitType(this, context)); + } + return this.visitExpression(ast, context); } - visitLiteralExpr(ast: LiteralExpr, context: any): any { return ast; } - visitExternalExpr(ast: ExternalExpr, context: any): any { return ast; } visitConditionalExpr(ast: ConditionalExpr, context: any): any { ast.condition.visitExpression(this, context); ast.trueCase.visitExpression(this, context); ast.falseCase !.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitNotExpr(ast: NotExpr, context: any): any { ast.condition.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitAssertNotNullExpr(ast: AssertNotNull, context: any): any { ast.condition.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitCastExpr(ast: CastExpr, context: any): any { ast.value.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitFunctionExpr(ast: FunctionExpr, context: any): any { this.visitAllStatements(ast.statements, context); - return ast; + return this.visitExpression(ast, context); } visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any { ast.lhs.visitExpression(this, context); ast.rhs.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitReadPropExpr(ast: ReadPropExpr, context: any): any { ast.receiver.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitReadKeyExpr(ast: ReadKeyExpr, context: any): any { ast.receiver.visitExpression(this, context); ast.index.visitExpression(this, context); - return ast; + return this.visitExpression(ast, context); } visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any { this.visitAllExpressions(ast.entries, context); - return ast; + return this.visitExpression(ast, context); } visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any { ast.entries.forEach((entry) => entry.value.visitExpression(this, context)); - return ast; + return this.visitExpression(ast, context); } visitCommaExpr(ast: CommaExpr, context: any): any { this.visitAllExpressions(ast.parts, context); + return this.visitExpression(ast, context); } visitAllExpressions(exprs: Expression[], context: any): void { exprs.forEach(expr => expr.visitExpression(this, context)); @@ -1010,10 +1034,16 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any { stmt.value.visitExpression(this, context); + if (stmt.type) { + stmt.type.visitType(this, context); + } return stmt; } visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any { this.visitAllStatements(stmt.statements, context); + if (stmt.type) { + stmt.type.visitType(this, context); + } return stmt; } visitExpressionStmt(stmt: ExpressionStatement, context: any): any { @@ -1078,6 +1108,20 @@ class _ReadVarVisitor extends RecursiveAstVisitor { } } +export function collectExternalReferences(stmts: Statement[]): ExternalReference[] { + const visitor = new _FindExternalReferencesVisitor(); + visitor.visitAllStatements(stmts, null); + return visitor.externalReferences; +} + +class _FindExternalReferencesVisitor extends RecursiveAstVisitor { + externalReferences: ExternalReference[] = []; + visitExternalExpr(e: ExternalExpr, context: any) { + this.externalReferences.push(e.value); + return super.visitExternalExpr(e, context); + } +} + export function applySourceSpanToStatementIfNeeded( stmt: Statement, sourceSpan: ParseSourceSpan | null): Statement { if (!sourceSpan) { diff --git a/packages/compiler/src/view_compiler/type_check_compiler.ts b/packages/compiler/src/view_compiler/type_check_compiler.ts index dc441c38c5..efa35e969f 100644 --- a/packages/compiler/src/view_compiler/type_check_compiler.ts +++ b/packages/compiler/src/view_compiler/type_check_compiler.ts @@ -17,8 +17,6 @@ import * as o from '../output/output_ast'; import {convertValueToOutputAst} from '../output/value_util'; import {ParseSourceSpan} from '../parse_util'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; -import {OutputContext} from '../util'; - /** * Generates code that is used to type check templates. @@ -26,23 +24,31 @@ import {OutputContext} from '../util'; export class TypeCheckCompiler { constructor(private options: AotCompilerOptions, private reflector: StaticReflector) {} + /** + * Important notes: + * - This must not produce new `import` statements, but only refer to types outside + * of the file via the variables provided via externalReferenceVars. + * This allows Typescript to reuse the old program's structure as no imports have changed. + * - This must not produce any exports, as this would pollute the .d.ts file + * and also violate the point above. + */ compileComponent( - outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[], - usedPipes: CompilePipeSummary[]): void { + component: CompileDirectiveMetadata, template: TemplateAst[], usedPipes: CompilePipeSummary[], + externalReferenceVars: Map): o.Statement[] { const pipes = new Map(); usedPipes.forEach(p => pipes.set(p.name, p.type.reference)); let embeddedViewCount = 0; const viewBuilderFactory = (parent: ViewBuilder | null): ViewBuilder => { const embeddedViewIndex = embeddedViewCount++; return new ViewBuilder( - this.options, this.reflector, outputCtx, parent, component.type.reference, - embeddedViewIndex, pipes, viewBuilderFactory); + this.options, this.reflector, externalReferenceVars, parent, component.type.reference, + component.isHost, embeddedViewIndex, pipes, viewBuilderFactory); }; const visitor = viewBuilderFactory(null); visitor.visitAll([], template); - outputCtx.statements.push(...visitor.build()); + return visitor.build(); } } @@ -60,9 +66,9 @@ interface Expression { value: AST; } +const DYNAMIC_VAR_NAME = '_any'; + class ViewBuilder implements TemplateAstVisitor, LocalResolver { - private outputVarTypes = new Map(); - private outputVarNames = new Map(); private refOutputVars = new Map(); private variables: VariableAst[] = []; private children: ViewBuilder[] = []; @@ -71,16 +77,23 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { constructor( private options: AotCompilerOptions, private reflector: StaticReflector, - private outputCtx: OutputContext, private parent: ViewBuilder|null, - private component: StaticSymbol, private embeddedViewIndex: number, - private pipes: Map, private viewBuilderFactory: ViewBuilderFactory) {} + private externalReferenceVars: Map, private parent: ViewBuilder|null, + private component: StaticSymbol, private isHostComponent: boolean, + private embeddedViewIndex: number, private pipes: Map, + private viewBuilderFactory: ViewBuilderFactory) {} - private getOrAddOutputVar(type: o.BuiltinTypeName|StaticSymbol): string { - let varName = this.outputVarNames.get(type); + private getOutputVar(type: o.BuiltinTypeName|StaticSymbol): string { + let varName: string|undefined; + if (type === this.component && this.isHostComponent) { + varName = DYNAMIC_VAR_NAME; + } else if (type instanceof StaticSymbol) { + varName = this.externalReferenceVars.get(type); + } else { + varName = DYNAMIC_VAR_NAME; + } if (!varName) { - varName = `_v${this.outputVarNames.size}`; - this.outputVarNames.set(type, varName); - this.outputVarTypes.set(varName, type); + throw new Error( + `Illegal State: referring to a type without a variable ${JSON.stringify(type)}`); } return varName; } @@ -92,15 +105,15 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { build(targetStatements: o.Statement[] = []): o.Statement[] { this.children.forEach((child) => child.build(targetStatements)); - - const viewStmts: o.Statement[] = []; + const viewStmts: o.Statement[] = + [o.variable(DYNAMIC_VAR_NAME).set(o.NULL_EXPR).toDeclStmt(o.DYNAMIC_TYPE)]; let bindingCount = 0; this.updates.forEach((expression) => { const {sourceSpan, context, value} = this.preprocessUpdateExpression(expression); const bindingId = `${bindingCount++}`; const nameResolver = context === this.component ? this : null; const {stmts, currValExpr} = convertPropertyBinding( - nameResolver, o.variable(this.getOrAddOutputVar(context)), value, bindingId); + nameResolver, o.variable(this.getOutputVar(context)), value, bindingId); stmts.push(new o.ExpressionStatement(currValExpr)); viewStmts.push(...stmts.map( (stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan))); @@ -110,21 +123,13 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { const bindingId = `${bindingCount++}`; const nameResolver = context === this.component ? this : null; const {stmts} = convertActionBinding( - nameResolver, o.variable(this.getOrAddOutputVar(context)), value, bindingId); + nameResolver, o.variable(this.getOutputVar(context)), value, bindingId); viewStmts.push(...stmts.map( (stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan))); }); const viewName = `_View_${this.component.name}_${this.embeddedViewIndex}`; - const params: o.FnParam[] = []; - this.outputVarNames.forEach((varName, varType) => { - const outputType = varType instanceof StaticSymbol ? - o.expressionType(this.outputCtx.importExpr(varType)) : - new o.BuiltinType(varType); - params.push(new o.FnParam(varName, outputType)); - }); - - const viewFactory = new o.DeclareFunctionStmt(viewName, params, viewStmts); + const viewFactory = new o.DeclareFunctionStmt(viewName, [], viewStmts); targetStatements.push(viewFactory); return targetStatements; } @@ -211,7 +216,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { getLocal(name: string): o.Expression|null { if (name == EventHandlerVars.event.name) { - return o.variable(this.getOrAddOutputVar(o.BuiltinTypeName.Dynamic)); + return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic)); } for (let currBuilder: ViewBuilder|null = this; currBuilder; currBuilder = currBuilder.parent) { let outputVarType: OutputVarType|undefined; @@ -225,7 +230,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { } } if (outputVarType != null) { - return o.variable(this.getOrAddOutputVar(outputVarType)); + return o.variable(this.getOutputVar(outputVarType)); } } return null; @@ -237,7 +242,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { throw new Error( `Illegal State: Could not find pipe ${name} in template of ${this.component}`); } - return this.getOrAddOutputVar(pipe); + return this.getOutputVar(pipe); } private preprocessUpdateExpression(expression: Expression): Expression { @@ -264,7 +269,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { if (this.options.fullTemplateTypeCheck) { return o.variable(this.pipeOutputVar(name)).callMethod('transform', args); } else { - return o.variable(this.getOrAddOutputVar(o.BuiltinTypeName.Dynamic)); + return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic)); } }, }, diff --git a/packages/compiler/test/aot/emit_stubs_spec.ts b/packages/compiler/test/aot/emit_stubs_spec.ts deleted file mode 100644 index 0f6513a297..0000000000 --- a/packages/compiler/test/aot/emit_stubs_spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @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 {MockDirectory, compile, expectNoDiagnostics, setup, toMockFileArray} from './test_util'; - -describe('aot stubs', () => { - let angularFiles = setup(); - - it('should create empty .ngfactory and .ngsummary files for every source file', () => { - const appDir = {'app.ts': `export const x = 1;`}; - const rootDir = {'app': appDir}; - const {genFiles} = compile( - [rootDir, angularFiles], - {postCompile: expectNoDiagnostics, stubsOnly: true, enableSummariesForJit: true}); - expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngfactory.ts')).toBeTruthy(); - expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngsummary.ts')).toBeTruthy(); - }); - - it('should create empty .ngstyle files for imported css files', () => { - const appDir = { - 'app.ts': ` - import {Component, NgModule} from '@angular/core'; - - @Component({ - template: '', - styleUrls: ['./style.css'] - }) - export class MyComp {} - - @NgModule({ - declarations: [MyComp] - }) - export class MyModule {} - export const x = 1; - `, - 'style.css': '' - }; - const rootDir = {'app': appDir}; - const {genFiles} = - compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true}); - expect(genFiles.find((f) => f.genFileUrl === '/app/style.css.shim.ngstyle.ts')).toBeTruthy(); - }); - - it('should create stub exports for NgModules of the right type', () => { - const appDir = { - 'app.module.ts': ` - import { NgModule } from '@angular/core'; - - @NgModule() - export class MyModule {} - `, - 'app.boot.ts': ` - import {NgModuleFactory} from '@angular/core'; - import {MyModuleNgFactory} from './app.module.ngfactory'; - import {MyModuleNgSummary} from './app.module.ngsummary'; - import {MyModule} from './app.module'; - - export const factory: NgModuleFactory = MyModuleNgFactory; - export const summary: () => any[] = MyModuleNgSummary; - ` - }; - const rootDir = {'app': appDir}; - compile( - [rootDir, angularFiles], - {postCompile: expectNoDiagnostics, stubsOnly: true, enableSummariesForJit: true}); - }); -}); diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 2e54338f45..82368119fc 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -633,7 +633,6 @@ export function compile( useSummaries?: boolean, preCompile?: (program: ts.Program) => void, postCompile?: (program: ts.Program) => void, - stubsOnly?: boolean, }& AotCompilerOptions = {}, tsOptions: ts.CompilerOptions = {}): {genFiles: GeneratedFile[], outDir: MockDirectory} { // when using summaries, always emit so the next step can use the results. @@ -656,8 +655,7 @@ export function compile( const {compiler, reflector} = createAotCompiler(aotHost, options); const analyzedModules = compiler.analyzeModulesSync(program.getSourceFiles().map(sf => sf.fileName)); - const genFiles = options.stubsOnly ? compiler.emitAllStubs(analyzedModules) : - compiler.emitAllImpls(analyzedModules); + const genFiles = compiler.emitAllImpls(analyzedModules); genFiles.forEach((file) => { const source = file.source || toTypeScript(file); if (isSource(file.genFileUrl)) { diff --git a/packages/compiler/test/directive_normalizer_spec.ts b/packages/compiler/test/directive_normalizer_spec.ts index 6c16c9ca3d..e907ad614b 100644 --- a/packages/compiler/test/directive_normalizer_spec.ts +++ b/packages/compiler/test/directive_normalizer_spec.ts @@ -10,23 +10,18 @@ import {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compi import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config'; import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer'; import {ResourceLoader} from '@angular/compiler/src/resource_loader'; -import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; import {ViewEncapsulation} from '@angular/core/src/metadata/view'; -import {TestBed} from '@angular/core/testing'; -import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; +import {TestBed, inject} from '@angular/core/testing'; import {noUndefined} from '../src/util'; -import {SpyResourceLoader} from './spies'; import {TEST_COMPILER_PROVIDERS} from './test_bindings'; const SOME_MODULE_URL = 'package:some/module/a.js'; const SOME_HTTP_MODULE_URL = 'http://some/module/a.js'; function normalizeTemplate(normalizer: DirectiveNormalizer, o: { - ngModuleType?: any; componentType?: any; moduleUrl?: string; template?: string | null; - templateUrl?: string | null; - styles?: string[]; + moduleUrl?: string; template?: string | null; templateUrl?: string | null; styles?: string[]; styleUrls?: string[]; interpolation?: [string, string] | null; encapsulation?: ViewEncapsulation | null; @@ -34,9 +29,9 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: { preserveWhitespaces?: boolean | null; }) { return normalizer.normalizeTemplate({ - ngModuleType: noUndefined(o.ngModuleType), - componentType: noUndefined(o.componentType), - moduleUrl: noUndefined(o.moduleUrl), + ngModuleType: null, + componentType: SomeComp, + moduleUrl: noUndefined(o.moduleUrl || SOME_MODULE_URL), template: noUndefined(o.template), templateUrl: noUndefined(o.templateUrl), styles: noUndefined(o.styles), @@ -48,131 +43,39 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: { }); } -function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: { - ngModuleType?: any; componentType?: any; moduleUrl?: string; template?: string | null; - templateUrl?: string | null; - styles?: string[]; - styleUrls?: string[]; - interpolation?: [string, string] | null; - encapsulation?: ViewEncapsulation | null; - animations?: CompileAnimationEntryMetadata[]; - preserveWhitespaces?: boolean | null; -}) { - return normalizer.normalizeTemplateOnly({ - ngModuleType: noUndefined(o.ngModuleType), - componentType: noUndefined(o.componentType), - moduleUrl: noUndefined(o.moduleUrl), - template: noUndefined(o.template), - templateUrl: noUndefined(o.templateUrl), - styles: noUndefined(o.styles), - styleUrls: noUndefined(o.styleUrls), - interpolation: noUndefined(o.interpolation), - encapsulation: noUndefined(o.encapsulation), - animations: noUndefined(o.animations), - preserveWhitespaces: noUndefined(o.preserveWhitespaces), - }); -} - -function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls, - externalStylesheets, animations, ngContentSelectors, - interpolation, isInline, preserveWhitespaces}: { - encapsulation?: ViewEncapsulation | null, - template?: string | null, - templateUrl?: string | null, - styles?: string[], - styleUrls?: string[], - externalStylesheets?: CompileStylesheetMetadata[], - ngContentSelectors?: string[], - animations?: any[], - interpolation?: [string, string] | null, - isInline?: boolean, - preserveWhitespaces?: boolean | null -}): CompileTemplateMetadata { - return new CompileTemplateMetadata({ - encapsulation: encapsulation || null, - template: template || null, - templateUrl: templateUrl || null, - htmlAst: null, - styles: styles || [], - styleUrls: styleUrls || [], - externalStylesheets: externalStylesheets || [], - ngContentSelectors: ngContentSelectors || [], - animations: animations || [], - interpolation: interpolation || null, - isInline: !!isInline, - preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)), - }); -} - -function normalizeLoadedTemplate( - normalizer: DirectiveNormalizer, o: { - ngModuleType?: any; componentType?: any; moduleUrl?: string; template?: string | null; - templateUrl?: string | null; - styles?: string[]; - styleUrls?: string[]; - interpolation?: [string, string] | null; - encapsulation?: ViewEncapsulation | null; - animations?: CompileAnimationEntryMetadata[]; - preserveWhitespaces?: boolean; - }, - template: string, templateAbsUrl: string) { - return normalizer.normalizeLoadedTemplate( - { - ngModuleType: o.ngModuleType || null, - componentType: o.componentType || null, - moduleUrl: o.moduleUrl || '', - template: o.template || null, - templateUrl: o.templateUrl || null, - styles: o.styles || [], - styleUrls: o.styleUrls || [], - interpolation: o.interpolation || null, - encapsulation: o.encapsulation || null, - animations: o.animations || [], - preserveWhitespaces: noUndefined(o.preserveWhitespaces), - }, - template, templateAbsUrl); -} - export function main() { describe('DirectiveNormalizer', () => { - beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); }); + let resourceLoaderSpy: jasmine.Spy; - describe('normalizeDirective', () => { + beforeEach(() => { + resourceLoaderSpy = + jasmine.createSpy('get').and.callFake((url: string) => `resource(${url})`); + const resourceLoader = {get: resourceLoaderSpy}; + TestBed.configureCompiler({ + providers: + [...TEST_COMPILER_PROVIDERS, {provide: ResourceLoader, useValue: resourceLoader}] + }); + }); + + describe('normalizeTemplate', () => { it('should throw if no template was specified', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - expect(() => normalizeTemplate(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - })) + expect(() => normalizeTemplate(normalizer, {})) .toThrowError('No template specified for component SomeComp'); })); it('should throw if template is not a string', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - expect(() => normalizeTemplate(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - template: {} - })) + expect(() => normalizeTemplate(normalizer, {template: {}})) .toThrowError('The template specified for component SomeComp is not a string'); })); it('should throw if templateUrl is not a string', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - expect(() => normalizeTemplate(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - templateUrl: {} - })) + expect(() => normalizeTemplate(normalizer, {templateUrl: {}})) .toThrowError('The templateUrl specified for component SomeComp is not a string'); })); it('should throw if both template and templateUrl are defined', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { expect(() => normalizeTemplate(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, template: '', templateUrl: '', })) @@ -181,29 +84,21 @@ export function main() { it('should throw if preserveWhitespaces is not a boolean', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { expect(() => normalizeTemplate(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, template: '', preserveWhitespaces: 'WRONG', })) .toThrowError( 'The preserveWhitespaces option for component SomeComp must be a boolean'); })); + }); - describe('normalizeTemplateOnly sync', () => { + describe('inline template', () => { + it('should store the template', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, + const template = normalizeTemplate(normalizer, { template: 'a', - templateUrl: null, - styles: [], - styleUrls: [] }); expect(template.template).toEqual('a'); expect(template.templateUrl).toEqual('package:some/module/a.js'); @@ -212,46 +107,26 @@ export function main() { it('should resolve styles on the annotation against the moduleUrl', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - }); + const template = normalizeTemplate( + normalizer, {template: '', styleUrls: ['test.css']}); expect(template.styleUrls).toEqual(['package:some/module/test.css']); })); - it('should resolve styles in the template against the moduleUrl', + it('should resolve styles in the template against the moduleUrl and add them to the styles', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: [] + const template = normalizeTemplate(normalizer, { + template: '', + styles: ['direct'], }); - expect(template.styleUrls).toEqual(['package:some/module/test.css']); + expect(template.styles).toEqual([ + 'direct', 'template ', 'resource(package:some/module/test.css)' + ]); })); it('should use ViewEncapsulation.Emulated by default', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - }); + const template = normalizeTemplate( + normalizer, {template: '', styleUrls: ['test.css']}); expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated); })); @@ -260,195 +135,105 @@ export function main() { [CompilerConfig, DirectiveNormalizer], (config: CompilerConfig, normalizer: DirectiveNormalizer) => { config.defaultEncapsulation = ViewEncapsulation.None; - const template = normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: undefined, - template: '', - templateUrl: undefined, - styles: [], - styleUrls: ['test.css'] - }); + const template = normalizeTemplate( + normalizer, {template: '', styleUrls: ['test.css']}); expect(template.encapsulation).toEqual(ViewEncapsulation.None); })); }); - describe('templateUrl', () => { + it('should load a template from a url that is resolved against moduleUrl', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + const template = normalizeTemplate( + normalizer, {templateUrl: 'sometplurl.html', styleUrls: ['test.css']}); + expect(template.template).toEqual('resource(package:some/module/sometplurl.html)'); + expect(template.templateUrl).toEqual('package:some/module/sometplurl.html'); + expect(template.isInline).toBe(false); + })); - it('should load a template from a url that is resolved against moduleUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, ResourceLoader], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, - resourceLoader: MockResourceLoader) => { - resourceLoader.expect('package:some/module/sometplurl.html', 'a'); - (>normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - template: null, - templateUrl: 'sometplurl.html', - styles: [], - styleUrls: ['test.css'] - })).then((template) => { - expect(template.template).toEqual('a'); - expect(template.templateUrl).toEqual('package:some/module/sometplurl.html'); - expect(template.isInline).toBe(false); - async.done(); - }); - resourceLoader.flush(); - })); + it('should resolve styles on the annotation against the moduleUrl', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + const template = normalizeTemplate( + normalizer, {templateUrl: 'tpl/sometplurl.html', styleUrls: ['test.css']}); + expect(template.styleUrls).toEqual(['package:some/module/test.css']); + })); - it('should resolve styles on the annotation against the moduleUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, ResourceLoader], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, - resourceLoader: MockResourceLoader) => { - resourceLoader.expect('package:some/module/tpl/sometplurl.html', ''); - (>normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - template: null, - templateUrl: 'tpl/sometplurl.html', - styles: [], - styleUrls: ['test.css'] - })).then((template) => { - expect(template.styleUrls).toEqual(['package:some/module/test.css']); - async.done(); - }); - resourceLoader.flush(); - })); + it('should resolve styles in the template against the templateUrl and add them to the styles', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + resourceLoaderSpy.and.callFake((url: string) => { + switch (url) { + case 'package:some/module/tpl/sometplurl.html': + return ''; + default: + return `resource(${url})`; + } + }); + const template = normalizeTemplate( + normalizer, {templateUrl: 'tpl/sometplurl.html', styles: ['direct']}); + expect(template.styles).toEqual([ + 'direct', 'template ', 'resource(package:some/module/tpl/test.css)' + ]); + })); - it('should resolve styles in the template against the templateUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, ResourceLoader], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, - resourceLoader: MockResourceLoader) => { - resourceLoader.expect( - 'package:some/module/tpl/sometplurl.html', ''); - (>normalizeTemplateOnly(normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - template: null, - templateUrl: 'tpl/sometplurl.html', - styles: [], - styleUrls: [] - })).then((template) => { - expect(template.styleUrls).toEqual(['package:some/module/tpl/test.css']); - async.done(); - }); - resourceLoader.flush(); - })); - - }); - - describe('normalizeExternalStylesheets', () => { - - beforeEach(() => { TestBed.configureCompiler({providers: [SpyResourceLoader.PROVIDE]}); }); + describe('externalStylesheets', () => { it('should load an external stylesheet', - inject( - [AsyncTestCompleter, DirectiveNormalizer, ResourceLoader], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, - resourceLoader: SpyResourceLoader) => { - programResourceLoaderSpy(resourceLoader, {'package:some/module/test.css': 'a'}); - (>normalizer.normalizeExternalStylesheets( - compileTemplateMetadata({ - template: '', - templateUrl: '', - styleUrls: ['package:some/module/test.css'] - }))) - .then((template) => { - expect(template.externalStylesheets.length).toBe(1); - expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({ - moduleUrl: 'package:some/module/test.css', - styles: ['a'], - styleUrls: [] - })); - async.done(); - }); - })); + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + const template = normalizeTemplate( + normalizer, {template: '', styleUrls: ['package:some/module/test.css']}); + expect(template.externalStylesheets.length).toBe(1); + expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({ + moduleUrl: 'package:some/module/test.css', + styles: ['resource(package:some/module/test.css)'], + })); + })); - it('should load stylesheets referenced by external stylesheets', - inject( - [AsyncTestCompleter, DirectiveNormalizer, ResourceLoader], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, - resourceLoader: SpyResourceLoader) => { - programResourceLoaderSpy(resourceLoader, { - 'package:some/module/test.css': 'a@import "test2.css"', - 'package:some/module/test2.css': 'b' - }); - (>normalizer.normalizeExternalStylesheets( - compileTemplateMetadata({ - template: '', - templateUrl: '', - styleUrls: ['package:some/module/test.css'] - }))) - .then((template) => { - expect(template.externalStylesheets.length).toBe(2); - expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({ - moduleUrl: 'package:some/module/test.css', - styles: ['a'], - styleUrls: ['package:some/module/test2.css'] - })); - expect(template.externalStylesheets[1]).toEqual(new CompileStylesheetMetadata({ - moduleUrl: 'package:some/module/test2.css', - styles: ['b'], - styleUrls: [] - })); - async.done(); - }); - })); + it('should load stylesheets referenced by external stylesheets and inline them', + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + resourceLoaderSpy.and.callFake((url: string) => { + switch (url) { + case 'package:some/module/test.css': + return 'a@import "test2.css"'; + case 'package:some/module/test2.css': + return 'b'; + default: + throw new Error(`Unexpected url ${url}`); + } + }); + const template = normalizeTemplate(normalizer, { + template: '', + styleUrls: ['package:some/module/test.css'], + }); + expect(template.externalStylesheets.length).toBe(1); + expect(template.externalStylesheets[0]) + .toEqual(new CompileStylesheetMetadata( + {moduleUrl: 'package:some/module/test.css', styles: ['a', 'b'], styleUrls: []})); + })); }); describe('caching', () => { it('should work for templateUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, ResourceLoader], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, - resourceLoader: MockResourceLoader) => { - resourceLoader.expect('package:some/module/cmp.html', 'a'); - const prenormMeta = { - ngModuleType: null as any, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - templateUrl: 'cmp.html', - }; - Promise - .all([ - normalizeTemplateOnly(normalizer, prenormMeta), - normalizeTemplateOnly(normalizer, prenormMeta) - ]) - .then((templates: CompileTemplateMetadata[]) => { - expect(templates[0].template).toEqual('a'); - expect(templates[1].template).toEqual('a'); - async.done(); - }); - resourceLoader.flush(); - })); + inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { + const prenormMeta = { + templateUrl: 'cmp.html', + }; + const template1 = normalizeTemplate(normalizer, prenormMeta); + const template2 = normalizeTemplate(normalizer, prenormMeta); + expect(template1.template).toEqual('resource(package:some/module/cmp.html)'); + expect(template2.template).toEqual('resource(package:some/module/cmp.html)'); + + expect(resourceLoaderSpy).toHaveBeenCalledTimes(1); + })); }); describe('normalizeLoadedTemplate', () => { it('should store the viewEncapsulation in the result', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const viewEncapsulation = ViewEncapsulation.Native; - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: viewEncapsulation, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + encapsulation: viewEncapsulation, + template: '', + }); expect(template.encapsulation).toBe(viewEncapsulation); })); @@ -456,281 +241,166 @@ export function main() { inject( [DirectiveNormalizer, CompilerConfig], (normalizer: DirectiveNormalizer, config: CompilerConfig) => { - const template = normalizeLoadedTemplate(normalizer, {}, '', ''); + const template = + normalizeTemplate(normalizer, {template: ''}); expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces); })); it('should store the preserveWhitespaces=false in the result', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = - normalizeLoadedTemplate(normalizer, {preserveWhitespaces: false}, '', ''); + const template = normalizeTemplate( + normalizer, {preserveWhitespaces: false, template: ''}); expect(template.preserveWhitespaces).toBe(false); })); it('should store the preserveWhitespaces=true in the result', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = - normalizeLoadedTemplate(normalizer, {preserveWhitespaces: true}, '', ''); + const template = normalizeTemplate( + normalizer, {preserveWhitespaces: true, template: ''}); expect(template.preserveWhitespaces).toBe(true); })); it('should keep the template as html', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - 'a', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: 'a', + }); expect(template.template).toEqual('a'); })); it('should collect ngContent', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '', + }); expect(template.ngContentSelectors).toEqual(['a']); })); it('should normalize ngContent wildcard selector', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '', - 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: + '', + }); expect(template.ngContentSelectors).toEqual(['*', '*', '*']); })); it('should collect top level styles in the template', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '', + }); expect(template.styles).toEqual(['a']); })); - it('should collect styles inside in elements', + it('should collect styles inside elements', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '
', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '
', + }); expect(template.styles).toEqual(['a']); })); - it('should collect styleUrls in the template', + it('should collect styleUrls in the template and add them to the styles', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/'); - expect(template.styleUrls).toEqual(['package:some/module/aUrl']); + const template = normalizeTemplate(normalizer, { + template: '', + }); + expect(template.styles).toEqual(['resource(package:some/module/aUrl)']); + expect(template.styleUrls).toEqual([]); })); - it('should collect styleUrls in elements', + it('should collect styleUrls in elements and add them to the styles', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '
', 'package:some/module/'); - expect(template.styleUrls).toEqual(['package:some/module/aUrl']); + const template = normalizeTemplate(normalizer, { + template: '
', + }); + expect(template.styles).toEqual(['resource(package:some/module/aUrl)']); + expect(template.styleUrls).toEqual([]); })); it('should ignore link elements with non stylesheet rel attribute', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '', + }); expect(template.styleUrls).toEqual([]); })); it('should ignore link elements with absolute urls but non package: scheme', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '', + }); expect(template.styleUrls).toEqual([]); })); - it('should extract @import style urls into styleAbsUrl', + it('should extract @import style urls and add them to the styles', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: ['@import "test.css";'], - styleUrls: [] - }, - '', 'package:some/module/id'); - expect(template.styles).toEqual(['']); - expect(template.styleUrls).toEqual(['package:some/module/test.css']); + const template = normalizeTemplate(normalizer, { + styles: ['@import "test.css";'], + template: '', + }); + expect(template.styles).toEqual(['', 'resource(package:some/module/test.css)']); + expect(template.styleUrls).toEqual([]); })); it('should not resolve relative urls in inline styles', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: ['.foo{background-image: url(\'double.jpg\');'], - styleUrls: [] - }, - '', 'package:some/module/id'); + const template = normalizeTemplate(normalizer, { + styles: ['.foo{background-image: url(\'double.jpg\');'], + template: '', + }); expect(template.styles).toEqual(['.foo{background-image: url(\'double.jpg\');']); })); it('should resolve relative style urls in styleUrls', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: ['test.css'] - }, - '', 'package:some/module/id'); + const template = normalizeTemplate(normalizer, { + styleUrls: ['test.css'], + template: '', + }); expect(template.styles).toEqual([]); expect(template.styleUrls).toEqual(['package:some/module/test.css']); })); it('should resolve relative style urls in styleUrls with http directive url', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_HTTP_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: ['test.css'] - }, - '', 'http://some/module/id'); + const template = normalizeTemplate(normalizer, { + moduleUrl: SOME_HTTP_MODULE_URL, + styleUrls: ['test.css'], + template: '', + }); expect(template.styles).toEqual([]); expect(template.styleUrls).toEqual(['http://some/module/test.css']); })); it('should normalize ViewEncapsulation.Emulated to ViewEncapsulation.None if there are no styles nor stylesheets', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: ViewEncapsulation.Emulated, - styles: [], - styleUrls: [] - }, - '', 'package:some/module/id'); + const template = normalizeTemplate(normalizer, { + encapsulation: ViewEncapsulation.Emulated, + template: '', + }); expect(template.encapsulation).toEqual(ViewEncapsulation.None); })); it('should ignore ng-content in elements with ngNonBindable', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - const template = normalizeLoadedTemplate( - normalizer, { - ngModuleType: null, - componentType: SomeComp, - moduleUrl: SOME_MODULE_URL, - encapsulation: null, - styles: [], - styleUrls: [] - }, - '
', - 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '
', + }); expect(template.ngContentSelectors).toEqual([]); })); it('should still collect
', 'package:some/module/'); + const template = normalizeTemplate(normalizer, { + template: '
', + }); expect(template.styles).toEqual(['div {color:red}']); })); }); }); } -function programResourceLoaderSpy(spy: SpyResourceLoader, results: {[key: string]: string}) { - spy.spy('get').and.callFake((url: string): Promise => { - const result = results[url]; - if (result) { - return Promise.resolve(result); - } else { - return Promise.reject(`Unknown mock url ${url}`); - } - }); -} - class SomeComp {} diff --git a/packages/compiler/test/output/output_ast_spec.ts b/packages/compiler/test/output/output_ast_spec.ts new file mode 100644 index 0000000000..f21895533c --- /dev/null +++ b/packages/compiler/test/output/output_ast_spec.ts @@ -0,0 +1,25 @@ +/** + * @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 o from '../../src/output/output_ast'; + +export function main() { + describe('OutputAst', () => { + describe('collectExternalReferences', () => { + it('should find expressions of variable types', () => { + const ref1 = new o.ExternalReference('aModule', 'name1'); + const ref2 = new o.ExternalReference('aModule', 'name2'); + const stmt = + o.variable('test').set(o.NULL_EXPR).toDeclStmt(o.importType(ref1, [o.importType( + ref2) !])); + + expect(o.collectExternalReferences([stmt])).toEqual([ref1, ref2]); + }); + }); + }); +} diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index 3a71cf6f28..a39407bfb9 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; +import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler'; import {CompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; @@ -104,9 +104,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost { new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config); result = this._resolver = new CompileMetadataResolver( - config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(), - elementSchemaRegistry, directiveNormalizer, new Console(), this._staticSymbolCache, - this.reflector, (error, type) => this.collectError(error, type && type.filePath)); + config, htmlParser, moduleResolver, directiveResolver, pipeResolver, + new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(), + this._staticSymbolCache, this.reflector, + (error, type) => this.collectError(error, type && type.filePath)); } return result; } @@ -146,12 +147,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost { 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; } diff --git a/packages/platform-browser-dynamic/src/compiler_factory.ts b/packages/platform-browser-dynamic/src/compiler_factory.ts index c2a2fb3cc0..d3c477cee9 100644 --- a/packages/platform-browser-dynamic/src/compiler_factory.ts +++ b/packages/platform-browser-dynamic/src/compiler_factory.ts @@ -34,13 +34,12 @@ export class CompilerImpl implements Compiler { private _delegate: JitCompiler; constructor( private _injector: Injector, private _metadataResolver: CompileMetadataResolver, - htmlParser: HtmlParser, templateParser: TemplateParser, styleCompiler: StyleCompiler, - viewCompiler: ViewCompiler, ngModuleCompiler: NgModuleCompiler, - summaryResolver: SummaryResolver>, compileReflector: CompileReflector, - compilerConfig: CompilerConfig, console: Console) { + templateParser: TemplateParser, styleCompiler: StyleCompiler, viewCompiler: ViewCompiler, + ngModuleCompiler: NgModuleCompiler, summaryResolver: SummaryResolver>, + compileReflector: CompileReflector, compilerConfig: CompilerConfig, console: Console) { this._delegate = new JitCompiler( - _metadataResolver, htmlParser, templateParser, styleCompiler, viewCompiler, - ngModuleCompiler, summaryResolver, compileReflector, compilerConfig, console, + _metadataResolver, templateParser, styleCompiler, viewCompiler, ngModuleCompiler, + summaryResolver, compileReflector, compilerConfig, console, this.getExtraNgModuleProviders.bind(this)); } @@ -128,7 +127,7 @@ export const COMPILER_PROVIDERS = [ I18NHtmlParser, Console] }, { provide: DirectiveNormalizer, deps: [ResourceLoader, UrlResolver, HtmlParser, CompilerConfig]}, - { provide: CompileMetadataResolver, deps: [CompilerConfig, NgModuleResolver, + { provide: CompileMetadataResolver, deps: [CompilerConfig, HtmlParser, NgModuleResolver, DirectiveResolver, PipeResolver, SummaryResolver, ElementSchemaRegistry, @@ -142,7 +141,7 @@ export const COMPILER_PROVIDERS = [ { provide: NgModuleCompiler, deps: [CompileReflector] }, { provide: CompilerConfig, useValue: new CompilerConfig()}, { provide: Compiler, useClass: CompilerImpl, deps: [Injector, CompileMetadataResolver, - HtmlParser, TemplateParser, StyleCompiler, + TemplateParser, StyleCompiler, ViewCompiler, NgModuleCompiler, SummaryResolver, CompileReflector, CompilerConfig, Console]},