From 79383ce1500e132a940da14d8525c92320f4b06c Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Thu, 10 Nov 2016 14:07:30 -0800 Subject: [PATCH] refactor(compiler): never create CompileDirectiveMetadata with not loaded resources (#12788) Part of #12787 --- modules/@angular/compiler-cli/src/codegen.ts | 4 +- .../@angular/compiler-cli/src/extractor.ts | 65 +- .../@angular/compiler/src/compile_metadata.ts | 47 +- .../compiler/src/directive_normalizer.ts | 104 ++- .../compiler/src/directive_resolver.ts | 5 + .../compiler/src/metadata_resolver.ts | 645 ++++++++++-------- .../compiler/src/ng_module_resolver.ts | 2 + .../@angular/compiler/src/offline_compiler.ts | 220 +++--- .../@angular/compiler/src/pipe_resolver.ts | 5 + .../@angular/compiler/src/runtime_compiler.ts | 185 ++--- .../test/directive_normalizer_spec.ts | 316 ++++++--- .../compiler/test/metadata_resolver_spec.ts | 479 ++++++++----- .../compiler/test/runtime_compiler_spec.ts | 3 +- .../test/browser/bootstrap_spec.ts | 20 +- 14 files changed, 1165 insertions(+), 935 deletions(-) diff --git a/modules/@angular/compiler-cli/src/codegen.ts b/modules/@angular/compiler-cli/src/codegen.ts index 0b271005ed..479eeee7f5 100644 --- a/modules/@angular/compiler-cli/src/codegen.ts +++ b/modules/@angular/compiler-cli/src/codegen.ts @@ -129,10 +129,10 @@ export class CodeGenerator { const resolver = new compiler.CompileMetadataResolver( new compiler.NgModuleResolver(staticReflector), new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), - elementSchemaRegistry, staticReflector); + elementSchemaRegistry, normalizer, staticReflector); // TODO(vicb): do not pass cliOptions.i18nFormat here const offlineCompiler = new compiler.OfflineCompiler( - resolver, normalizer, tmplParser, new compiler.StyleCompiler(urlResolver), + resolver, tmplParser, new compiler.StyleCompiler(urlResolver), new compiler.ViewCompiler(config, elementSchemaRegistry), new compiler.DirectiveWrapperCompiler( config, expressionParser, elementSchemaRegistry, console), diff --git a/modules/@angular/compiler-cli/src/extractor.ts b/modules/@angular/compiler-cli/src/extractor.ts index 4b9aacf99c..8d9d5bfac5 100644 --- a/modules/@angular/compiler-cli/src/extractor.ts +++ b/modules/@angular/compiler-cli/src/extractor.ts @@ -28,50 +28,40 @@ export class Extractor { private options: tsc.AngularCompilerOptions, private program: ts.Program, public host: ts.CompilerHost, private staticReflector: StaticReflector, private messageBundle: compiler.MessageBundle, private reflectorHost: ReflectorHost, - private metadataResolver: compiler.CompileMetadataResolver, - private directiveNormalizer: compiler.DirectiveNormalizer) {} + private metadataResolver: compiler.CompileMetadataResolver) {} extract(): Promise { const programSymbols: StaticSymbol[] = extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options); - const files = - compiler.analyzeNgModules(programSymbols, {transitiveModules: true}, this.metadataResolver) - .files; - const errors: compiler.ParseError[] = []; - const filePromises: Promise[] = []; + return compiler + .analyzeNgModules(programSymbols, {transitiveModules: true}, this.metadataResolver) + .then(({files}) => { + const errors: compiler.ParseError[] = []; - files.forEach(file => { - const cmpPromises: Promise[] = []; - file.directives.forEach(directiveType => { - const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType); - if (dirMeta.isComponent) { - cmpPromises.push(this.directiveNormalizer.normalizeDirective(dirMeta).asyncResult); - } - }); - - if (cmpPromises.length) { - const done = - Promise.all(cmpPromises).then((compMetas: compiler.CompileDirectiveMetadata[]) => { - compMetas.forEach(compMeta => { - const html = compMeta.template.template; - const interpolationConfig = - compiler.InterpolationConfig.fromArray(compMeta.template.interpolation); - errors.push(...this.messageBundle.updateFromTemplate( - html, file.srcUrl, interpolationConfig)); - }); + files.forEach(file => { + const compMetas: compiler.CompileDirectiveMetadata[] = []; + file.directives.forEach(directiveType => { + const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType); + if (dirMeta && dirMeta.isComponent) { + compMetas.push(dirMeta); + } }); + compMetas.forEach(compMeta => { + const html = compMeta.template.template; + const interpolationConfig = + compiler.InterpolationConfig.fromArray(compMeta.template.interpolation); + errors.push( + ...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig)); + }); + }); - filePromises.push(done); - } - }); + if (errors.length) { + throw new Error(errors.map(e => e.toString()).join('\n')); + } - - if (errors.length) { - throw new Error(errors.map(e => e.toString()).join('\n')); - } - - return Promise.all(filePromises).then(_ => this.messageBundle); + return this.messageBundle; + }); } static create( @@ -98,13 +88,12 @@ export class Extractor { const resolver = new compiler.CompileMetadataResolver( new compiler.NgModuleResolver(staticReflector), new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), - elementSchemaRegistry, staticReflector); + elementSchemaRegistry, normalizer, staticReflector); // TODO(vicb): implicit tags & attributes let messageBundle = new compiler.MessageBundle(htmlParser, [], {}); return new Extractor( - options, program, compilerHost, staticReflector, messageBundle, reflectorHost, resolver, - normalizer); + options, program, compilerHost, staticReflector, messageBundle, reflectorHost, resolver); } } \ No newline at end of file diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index e2ca43f3f6..bec0a89c89 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -326,9 +326,7 @@ export class CompileDirectiveMetadata implements CompileMetadataWithIdentifier { Array, queries?: CompileQueryMetadata[], viewQueries?: CompileQueryMetadata[], - entryComponents?: CompileTypeMetadata[], - viewDirectives?: CompileTypeMetadata[], - viewPipes?: CompileTypeMetadata[], + entryComponents?: CompileIdentifierMetadata[], template?: CompileTemplateMetadata } = {}): CompileDirectiveMetadata { var hostListeners: {[key: string]: string} = {}; @@ -397,7 +395,7 @@ export class CompileDirectiveMetadata implements CompileMetadataWithIdentifier { queries: CompileQueryMetadata[]; viewQueries: CompileQueryMetadata[]; // Note: Need to keep types here to prevent cycles! - entryComponents: CompileTypeMetadata[]; + entryComponents: CompileIdentifierMetadata[]; template: CompileTemplateMetadata; @@ -421,9 +419,7 @@ export class CompileDirectiveMetadata implements CompileMetadataWithIdentifier { Array, queries?: CompileQueryMetadata[], viewQueries?: CompileQueryMetadata[], - entryComponents?: CompileTypeMetadata[], - viewDirectives?: CompileTypeMetadata[], - viewPipes?: CompileTypeMetadata[], + entryComponents?: CompileIdentifierMetadata[], template?: CompileTemplateMetadata, } = {}) { this.type = type; @@ -506,13 +502,13 @@ export class CompilePipeMetadata implements CompileMetadataWithIdentifier { */ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier { type: CompileTypeMetadata; - declaredDirectives: CompileDirectiveMetadata[]; - exportedDirectives: CompileDirectiveMetadata[]; - declaredPipes: CompilePipeMetadata[]; - exportedPipes: CompilePipeMetadata[]; + declaredDirectives: CompileIdentifierMetadata[]; + exportedDirectives: CompileIdentifierMetadata[]; + declaredPipes: CompileIdentifierMetadata[]; + exportedPipes: CompileIdentifierMetadata[]; // Note: See CompileDirectiveMetadata.entryComponents why this has to be a type. - entryComponents: CompileTypeMetadata[]; - bootstrapComponents: CompileTypeMetadata[]; + entryComponents: CompileIdentifierMetadata[]; + bootstrapComponents: CompileIdentifierMetadata[]; providers: CompileProviderMetadata[]; importedModules: CompileNgModuleMetadata[]; @@ -529,12 +525,12 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier { type?: CompileTypeMetadata, providers?: Array, - declaredDirectives?: CompileDirectiveMetadata[], - exportedDirectives?: CompileDirectiveMetadata[], - declaredPipes?: CompilePipeMetadata[], - exportedPipes?: CompilePipeMetadata[], - entryComponents?: CompileTypeMetadata[], - bootstrapComponents?: CompileTypeMetadata[], + declaredDirectives?: CompileIdentifierMetadata[], + exportedDirectives?: CompileIdentifierMetadata[], + declaredPipes?: CompileIdentifierMetadata[], + exportedPipes?: CompileIdentifierMetadata[], + entryComponents?: CompileIdentifierMetadata[], + bootstrapComponents?: CompileIdentifierMetadata[], importedModules?: CompileNgModuleMetadata[], exportedModules?: CompileNgModuleMetadata[], transitiveModule?: TransitiveCompileNgModuleMetadata, @@ -560,15 +556,16 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier { } export class TransitiveCompileNgModuleMetadata { - directivesSet = new Set>(); - pipesSet = new Set>(); + directivesSet = new Set(); + pipesSet = new Set(); constructor( public modules: CompileNgModuleMetadata[], public providers: CompileProviderMetadata[], - public entryComponents: CompileTypeMetadata[], public directives: CompileDirectiveMetadata[], - public pipes: CompilePipeMetadata[]) { - directives.forEach(dir => this.directivesSet.add(dir.type.reference)); - pipes.forEach(pipe => this.pipesSet.add(pipe.type.reference)); + public entryComponents: CompileIdentifierMetadata[], + public directives: CompileIdentifierMetadata[], public pipes: CompileIdentifierMetadata[], + public loadingPromises: Promise[]) { + directives.forEach(dir => this.directivesSet.add(dir.reference)); + pipes.forEach(pipe => this.pipesSet.add(pipe.reference)); } } diff --git a/modules/@angular/compiler/src/directive_normalizer.ts b/modules/@angular/compiler/src/directive_normalizer.ts index f8b7f062ff..382844b1c8 100644 --- a/modules/@angular/compiler/src/directive_normalizer.ts +++ b/modules/@angular/compiler/src/directive_normalizer.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, ViewEncapsulation} from '@angular/core'; +import {Component, Injectable, ViewEncapsulation} from '@angular/core'; -import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from './compile_metadata'; +import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from './compile_metadata'; import {CompilerConfig} from './config'; -import {isBlank, isPresent} from './facade/lang'; +import {isBlank, isPresent, stringify} from './facade/lang'; import * as html from './ml_parser/ast'; import {HtmlParser} from './ml_parser/html_parser'; import {InterpolationConfig} from './ml_parser/interpolation_config'; @@ -20,6 +20,18 @@ import {PreparsedElementType, preparseElement} from './template_parser/template_ import {UrlResolver} from './url_resolver'; import {SyncAsyncResult} from './util'; +export interface PrenormalizedTemplateMetadata { + componentType: any; + moduleUrl: string; + template?: string; + templateUrl?: string; + styles?: string[]; + styleUrls?: string[]; + interpolation?: [string, string]; + encapsulation?: ViewEncapsulation; + animations?: CompileAnimationEntryMetadata[]; +} + @Injectable() export class DirectiveNormalizer { private _resourceLoaderCache = new Map>(); @@ -48,65 +60,56 @@ export class DirectiveNormalizer { return result; } - normalizeDirective(directive: CompileDirectiveMetadata): - SyncAsyncResult { - if (!directive.isComponent) { - // For non components there is nothing to be normalized yet. - return new SyncAsyncResult(directive, Promise.resolve(directive)); - } + normalizeTemplate(prenormData: PrenormalizedTemplateMetadata): + SyncAsyncResult { let normalizedTemplateSync: CompileTemplateMetadata = null; let normalizedTemplateAsync: Promise; - if (isPresent(directive.template.template)) { - normalizedTemplateSync = this.normalizeTemplateSync(directive.type, directive.template); + if (isPresent(prenormData.template)) { + normalizedTemplateSync = this.normalizeTemplateSync(prenormData); normalizedTemplateAsync = Promise.resolve(normalizedTemplateSync); - } else if (directive.template.templateUrl) { - normalizedTemplateAsync = this.normalizeTemplateAsync(directive.type, directive.template); + } else if (prenormData.templateUrl) { + normalizedTemplateAsync = this.normalizeTemplateAsync(prenormData); } else { - throw new Error(`No template specified for component ${directive.type.name}`); + throw new Error( + `No template specified for component ${stringify(prenormData.componentType)}`); } + if (normalizedTemplateSync && normalizedTemplateSync.styleUrls.length === 0) { // sync case - let normalizedDirective = _cloneDirectiveWithTemplate(directive, normalizedTemplateSync); - return new SyncAsyncResult(normalizedDirective, Promise.resolve(normalizedDirective)); + return new SyncAsyncResult(normalizedTemplateSync); } else { // async case return new SyncAsyncResult( - null, - normalizedTemplateAsync - .then((normalizedTemplate) => this.normalizeExternalStylesheets(normalizedTemplate)) - .then( - (normalizedTemplate) => - _cloneDirectiveWithTemplate(directive, normalizedTemplate))); + null, normalizedTemplateAsync.then( + (normalizedTemplate) => this.normalizeExternalStylesheets(normalizedTemplate))); } } - normalizeTemplateSync(directiveType: CompileTypeMetadata, template: CompileTemplateMetadata): - CompileTemplateMetadata { - return this.normalizeLoadedTemplate( - directiveType, template, template.template, directiveType.moduleUrl); + normalizeTemplateSync(prenomData: PrenormalizedTemplateMetadata): CompileTemplateMetadata { + return this.normalizeLoadedTemplate(prenomData, prenomData.template, prenomData.moduleUrl); } - normalizeTemplateAsync(directiveType: CompileTypeMetadata, template: CompileTemplateMetadata): + normalizeTemplateAsync(prenomData: PrenormalizedTemplateMetadata): Promise { - let templateUrl = this._urlResolver.resolve(directiveType.moduleUrl, template.templateUrl); + let templateUrl = this._urlResolver.resolve(prenomData.moduleUrl, prenomData.templateUrl); return this._fetch(templateUrl) - .then((value) => this.normalizeLoadedTemplate(directiveType, template, value, templateUrl)); + .then((value) => this.normalizeLoadedTemplate(prenomData, value, templateUrl)); } normalizeLoadedTemplate( - directiveType: CompileTypeMetadata, templateMeta: CompileTemplateMetadata, template: string, + prenomData: PrenormalizedTemplateMetadata, template: string, templateAbsUrl: string): CompileTemplateMetadata { - const interpolationConfig = InterpolationConfig.fromArray(templateMeta.interpolation); - const rootNodesAndErrors = - this._htmlParser.parse(template, directiveType.name, false, interpolationConfig); + const interpolationConfig = InterpolationConfig.fromArray(prenomData.interpolation); + const rootNodesAndErrors = this._htmlParser.parse( + template, stringify(prenomData.componentType), false, interpolationConfig); if (rootNodesAndErrors.errors.length > 0) { const errorString = rootNodesAndErrors.errors.join('\n'); throw new Error(`Template parse errors:\n${errorString}`); } const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({ - styles: templateMeta.styles, - styleUrls: templateMeta.styleUrls, - moduleUrl: directiveType.moduleUrl + styles: prenomData.styles, + styleUrls: prenomData.styleUrls, + moduleUrl: prenomData.moduleUrl })); const visitor = new TemplatePreparseVisitor(); @@ -114,7 +117,7 @@ export class DirectiveNormalizer { const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata( {styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl})); - let encapsulation = templateMeta.encapsulation; + let encapsulation = prenomData.encapsulation; if (isBlank(encapsulation)) { encapsulation = this._config.defaultEncapsulation; } @@ -131,10 +134,9 @@ export class DirectiveNormalizer { encapsulation, template, templateUrl: templateAbsUrl, styles, styleUrls, - externalStylesheets: templateMeta.externalStylesheets, ngContentSelectors: visitor.ngContentSelectors, - animations: templateMeta.animations, - interpolation: templateMeta.interpolation, + animations: prenomData.animations, + interpolation: prenomData.interpolation, }); } @@ -231,25 +233,3 @@ class TemplatePreparseVisitor implements html.Visitor { visitExpansion(ast: html.Expansion, context: any): any { return null; } visitExpansionCase(ast: html.ExpansionCase, context: any): any { return null; } } - -function _cloneDirectiveWithTemplate( - directive: CompileDirectiveMetadata, - template: CompileTemplateMetadata): CompileDirectiveMetadata { - return new CompileDirectiveMetadata({ - type: directive.type, - isComponent: directive.isComponent, - selector: directive.selector, - exportAs: directive.exportAs, - changeDetection: directive.changeDetection, - inputs: directive.inputs, - outputs: directive.outputs, - hostListeners: directive.hostListeners, - hostProperties: directive.hostProperties, - hostAttributes: directive.hostAttributes, - providers: directive.providers, - viewProviders: directive.viewProviders, - queries: directive.queries, - viewQueries: directive.viewQueries, - entryComponents: directive.entryComponents, template, - }); -} diff --git a/modules/@angular/compiler/src/directive_resolver.ts b/modules/@angular/compiler/src/directive_resolver.ts index 3d54f80b17..d50f5fdd6e 100644 --- a/modules/@angular/compiler/src/directive_resolver.ts +++ b/modules/@angular/compiler/src/directive_resolver.ts @@ -24,6 +24,11 @@ import {splitAtColon} from './util'; export class DirectiveResolver { constructor(private _reflector: ReflectorReader = reflector) {} + isDirective(type: Type) { + const typeMetadata = this._reflector.annotations(resolveForwardRef(type)); + return typeMetadata && typeMetadata.some(isDirectiveMetadata); + } + /** * Return {@link Directive} for a given `Type`. */ diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index 3c0c9e04c0..61009f787c 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -10,17 +10,27 @@ import {AnimationAnimateMetadata, AnimationEntryMetadata, AnimationGroupMetadata import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions'; import * as cpl from './compile_metadata'; +import {DirectiveNormalizer} from './directive_normalizer'; import {DirectiveResolver} from './directive_resolver'; +import {ListWrapper} from './facade/collection'; import {isBlank, isPresent, stringify} from './facade/lang'; import {Identifiers, resolveIdentifierToken} from './identifiers'; import {hasLifecycleHook} from './lifecycle_reflector'; import {NgModuleResolver} from './ng_module_resolver'; import {PipeResolver} from './pipe_resolver'; -import {LIFECYCLE_HOOKS_VALUES, ReflectorReader, reflector} from './private_import_core'; +import {ComponentStillLoadingError, LIFECYCLE_HOOKS_VALUES, ReflectorReader, reflector} from './private_import_core'; import {ElementSchemaRegistry} from './schema/element_schema_registry'; import {getUrlScheme} from './url_resolver'; -import {MODULE_SUFFIX, ValueTransformer, sanitizeIdentifier, visitValue} from './util'; +import {MODULE_SUFFIX, SyncAsyncResult, ValueTransformer, sanitizeIdentifier, visitValue} from './util'; + +// Design notes: +// - don't lazily create metadata: +// For some metadata, we need to do async work sometimes, +// so the user has to kick off this loading. +// But we want to report errors even when the async work is +// not required to check that the user would have been able +// to wait correctly. @Injectable() export class CompileMetadataResolver { private _directiveCache = new Map, cpl.CompileDirectiveMetadata>(); @@ -33,6 +43,7 @@ export class CompileMetadataResolver { constructor( private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver, private _pipeResolver: PipeResolver, private _schemaRegistry: ElementSchemaRegistry, + private _directiveNormalizer: DirectiveNormalizer, private _reflector: ReflectorReader = reflector) {} private sanitizeTokenName(token: any): string { @@ -50,11 +61,15 @@ export class CompileMetadataResolver { } clearCacheFor(type: Type) { + const dirMeta = this._directiveCache.get(type); this._directiveCache.delete(type); this._pipeCache.delete(type); this._ngModuleOfTypes.delete(type); // Clear all of the NgModule as they contain transitive information! this._ngModuleCache.clear(); + if (dirMeta) { + this._directiveNormalizer.clearCacheFor(dirMeta); + } } clearCache() { @@ -62,50 +77,53 @@ export class CompileMetadataResolver { this._pipeCache.clear(); this._ngModuleCache.clear(); this._ngModuleOfTypes.clear(); + this._directiveNormalizer.clearCache(); } getAnimationEntryMetadata(entry: AnimationEntryMetadata): cpl.CompileAnimationEntryMetadata { - const defs = entry.definitions.map(def => this.getAnimationStateMetadata(def)); + const defs = entry.definitions.map(def => this._getAnimationStateMetadata(def)); return new cpl.CompileAnimationEntryMetadata(entry.name, defs); } - getAnimationStateMetadata(value: AnimationStateMetadata): cpl.CompileAnimationStateMetadata { + private _getAnimationStateMetadata(value: AnimationStateMetadata): + cpl.CompileAnimationStateMetadata { if (value instanceof AnimationStateDeclarationMetadata) { - const styles = this.getAnimationStyleMetadata(value.styles); + const styles = this._getAnimationStyleMetadata(value.styles); return new cpl.CompileAnimationStateDeclarationMetadata(value.stateNameExpr, styles); } if (value instanceof AnimationStateTransitionMetadata) { return new cpl.CompileAnimationStateTransitionMetadata( - value.stateChangeExpr, this.getAnimationMetadata(value.steps)); + value.stateChangeExpr, this._getAnimationMetadata(value.steps)); } return null; } - getAnimationStyleMetadata(value: AnimationStyleMetadata): cpl.CompileAnimationStyleMetadata { + private _getAnimationStyleMetadata(value: AnimationStyleMetadata): + cpl.CompileAnimationStyleMetadata { return new cpl.CompileAnimationStyleMetadata(value.offset, value.styles); } - getAnimationMetadata(value: AnimationMetadata): cpl.CompileAnimationMetadata { + private _getAnimationMetadata(value: AnimationMetadata): cpl.CompileAnimationMetadata { if (value instanceof AnimationStyleMetadata) { - return this.getAnimationStyleMetadata(value); + return this._getAnimationStyleMetadata(value); } if (value instanceof AnimationKeyframesSequenceMetadata) { return new cpl.CompileAnimationKeyframesSequenceMetadata( - value.steps.map(entry => this.getAnimationStyleMetadata(entry))); + value.steps.map(entry => this._getAnimationStyleMetadata(entry))); } if (value instanceof AnimationAnimateMetadata) { const animateData = this - .getAnimationMetadata(value.styles); + ._getAnimationMetadata(value.styles); return new cpl.CompileAnimationAnimateMetadata(value.timings, animateData); } if (value instanceof AnimationWithStepsMetadata) { - const steps = value.steps.map(step => this.getAnimationMetadata(step)); + const steps = value.steps.map(step => this._getAnimationMetadata(step)); if (value instanceof AnimationGroupMetadata) { return new cpl.CompileAnimationGroupMetadata(steps); @@ -116,52 +134,35 @@ export class CompileMetadataResolver { return null; } - getDirectiveMetadata(directiveType: any, throwIfNotFound = true): cpl.CompileDirectiveMetadata { + private _loadDirectiveMetadata(directiveType: any, isSync: boolean): Promise { + if (this._directiveCache.has(directiveType)) { + return; + } directiveType = resolveForwardRef(directiveType); - let meta = this._directiveCache.get(directiveType); - if (!meta) { - const dirMeta = this._directiveResolver.resolve(directiveType, throwIfNotFound); - if (!dirMeta) { - return null; - } - let templateMeta: cpl.CompileTemplateMetadata = null; + const dirMeta = this._directiveResolver.resolve(directiveType); + if (!dirMeta) { + return null; + } + let moduleUrl = staticTypeModuleUrl(directiveType); + + const createDirectiveMetadata = (templateMeta: cpl.CompileTemplateMetadata) => { let changeDetectionStrategy: ChangeDetectionStrategy = null; let viewProviders: Array = []; - let moduleUrl = staticTypeModuleUrl(directiveType); - let entryComponentMetadata: cpl.CompileTypeMetadata[] = []; + let entryComponentMetadata: cpl.CompileIdentifierMetadata[] = []; let selector = dirMeta.selector; if (dirMeta instanceof Component) { // Component - assertArrayOfStrings('styles', dirMeta.styles); - assertArrayOfStrings('styleUrls', dirMeta.styleUrls); - assertInterpolationSymbols('interpolation', dirMeta.interpolation); - - const animations = dirMeta.animations ? - dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) : - null; - - templateMeta = new cpl.CompileTemplateMetadata({ - encapsulation: dirMeta.encapsulation, - template: dirMeta.template, - templateUrl: dirMeta.templateUrl, - styles: dirMeta.styles, - styleUrls: dirMeta.styleUrls, - animations: animations, - interpolation: dirMeta.interpolation - }); - changeDetectionStrategy = dirMeta.changeDetection; if (dirMeta.viewProviders) { - viewProviders = this.getProvidersMetadata( + viewProviders = this._getProvidersMetadata( dirMeta.viewProviders, entryComponentMetadata, `viewProviders for "${stringify(directiveType)}"`); } - moduleUrl = componentModuleUrl(this._reflector, directiveType, dirMeta); if (dirMeta.entryComponents) { entryComponentMetadata = flattenAndDedupeArray(dirMeta.entryComponents) - .map((type) => this.getTypeMetadata(type, staticTypeModuleUrl(type))) + .map((type) => this._getIdentifierMetadata(type, staticTypeModuleUrl(type))) .concat(entryComponentMetadata); } if (!selector) { @@ -176,22 +177,22 @@ export class CompileMetadataResolver { let providers: Array = []; if (isPresent(dirMeta.providers)) { - providers = this.getProvidersMetadata( + providers = this._getProvidersMetadata( dirMeta.providers, entryComponentMetadata, `providers for "${stringify(directiveType)}"`); } let queries: cpl.CompileQueryMetadata[] = []; let viewQueries: cpl.CompileQueryMetadata[] = []; if (isPresent(dirMeta.queries)) { - queries = this.getQueriesMetadata(dirMeta.queries, false, directiveType); - viewQueries = this.getQueriesMetadata(dirMeta.queries, true, directiveType); + queries = this._getQueriesMetadata(dirMeta.queries, false, directiveType); + viewQueries = this._getQueriesMetadata(dirMeta.queries, true, directiveType); } - meta = cpl.CompileDirectiveMetadata.create({ + const meta = cpl.CompileDirectiveMetadata.create({ selector: selector, exportAs: dirMeta.exportAs, isComponent: !!templateMeta, - type: this.getTypeMetadata(directiveType, moduleUrl), + type: this._getTypeMetadata(directiveType, moduleUrl), template: templateMeta, changeDetection: changeDetectionStrategy, inputs: dirMeta.inputs, @@ -204,189 +205,278 @@ export class CompileMetadataResolver { entryComponents: entryComponentMetadata }); this._directiveCache.set(directiveType, meta); + return meta; + }; + + if (dirMeta instanceof Component) { + // component + moduleUrl = componentModuleUrl(this._reflector, directiveType, dirMeta); + assertArrayOfStrings('styles', dirMeta.styles); + assertArrayOfStrings('styleUrls', dirMeta.styleUrls); + assertInterpolationSymbols('interpolation', dirMeta.interpolation); + + const animations = dirMeta.animations ? + dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) : + null; + + const templateMeta = this._directiveNormalizer.normalizeTemplate({ + componentType: directiveType, + moduleUrl: moduleUrl, + encapsulation: dirMeta.encapsulation, + template: dirMeta.template, + templateUrl: dirMeta.templateUrl, + styles: dirMeta.styles, + styleUrls: dirMeta.styleUrls, + animations: animations, + interpolation: dirMeta.interpolation + }); + if (templateMeta.syncResult) { + createDirectiveMetadata(templateMeta.syncResult); + return null; + } else { + if (isSync) { + throw new ComponentStillLoadingError(directiveType); + } + return templateMeta.asyncResult.then(createDirectiveMetadata); + } + } else { + // directive + createDirectiveMetadata(null); + return null; } - return meta; } - getNgModuleMetadata(moduleType: any, throwIfNotFound = true): cpl.CompileNgModuleMetadata { + /** + * Gets the metadata for the given directive. + * This assumes `loadNgModuleMetadata` has been called first. + */ + getDirectiveMetadata(directiveType: any): cpl.CompileDirectiveMetadata { + const dirMeta = this._directiveCache.get(directiveType); + if (!dirMeta) { + throw new Error( + `Illegal state: getDirectiveMetadata can only be called after loadNgModuleMetadata for a module that declares it. Directive ${stringify(directiveType)}.`); + } + return dirMeta; + } + + isDirective(type: any) { return this._directiveResolver.isDirective(type); } + + isPipe(type: any) { return this._pipeResolver.isPipe(type); } + + /** + * Gets the metadata for the given module. + * This assumes `loadNgModuleMetadata` has been called first. + */ + getNgModuleMetadata(moduleType: any): cpl.CompileNgModuleMetadata { + const modMeta = this._ngModuleCache.get(moduleType); + if (!modMeta) { + throw new Error( + `Illegal state: getNgModuleMetadata can only be called after loadNgModuleMetadata. Module ${stringify(moduleType)}.`); + } + return modMeta; + } + + /** + * Loads an NgModule and all of its directives. This includes loading the exported directives of + * imported modules, + * but not private directives of imported modules. + */ + loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true): + {ngModule: cpl.CompileNgModuleMetadata, loading: Promise} { + const ngModule = this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound); + const loading = + ngModule ? Promise.all(ngModule.transitiveModule.loadingPromises) : Promise.resolve(null); + return {ngModule, loading}; + } + + private _loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true): + cpl.CompileNgModuleMetadata { moduleType = resolveForwardRef(moduleType); let compileMeta = this._ngModuleCache.get(moduleType); - if (!compileMeta) { - const meta = this._ngModuleResolver.resolve(moduleType, throwIfNotFound); - if (!meta) { - return null; - } - const declaredDirectives: cpl.CompileDirectiveMetadata[] = []; - const exportedDirectives: cpl.CompileDirectiveMetadata[] = []; - const declaredPipes: cpl.CompilePipeMetadata[] = []; - const exportedPipes: cpl.CompilePipeMetadata[] = []; - const importedModules: cpl.CompileNgModuleMetadata[] = []; - const exportedModules: cpl.CompileNgModuleMetadata[] = []; - const providers: any[] = []; - const entryComponents: cpl.CompileTypeMetadata[] = []; - const bootstrapComponents: cpl.CompileTypeMetadata[] = []; - const schemas: SchemaMetadata[] = []; - - if (meta.imports) { - flattenAndDedupeArray(meta.imports).forEach((importedType) => { - let importedModuleType: Type; - if (isValidType(importedType)) { - importedModuleType = importedType; - } else if (importedType && importedType.ngModule) { - const moduleWithProviders: ModuleWithProviders = importedType; - importedModuleType = moduleWithProviders.ngModule; - if (moduleWithProviders.providers) { - providers.push(...this.getProvidersMetadata( - moduleWithProviders.providers, entryComponents, - `provider for the NgModule '${stringify(importedModuleType)}'`)); - } - } - - if (importedModuleType) { - const importedMeta = this.getNgModuleMetadata(importedModuleType, false); - if (importedMeta === null) { - throw new Error( - `Unexpected ${this._getTypeDescriptor(importedType)} '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`); - } - importedModules.push(importedMeta); - } else { - throw new Error( - `Unexpected value '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`); - } - }); - } - - if (meta.exports) { - flattenAndDedupeArray(meta.exports).forEach((exportedType) => { - if (!isValidType(exportedType)) { - throw new Error( - `Unexpected value '${stringify(exportedType)}' exported by the module '${stringify(moduleType)}'`); - } - let exportedDirMeta: cpl.CompileDirectiveMetadata; - let exportedPipeMeta: cpl.CompilePipeMetadata; - let exportedModuleMeta: cpl.CompileNgModuleMetadata; - if (exportedDirMeta = this.getDirectiveMetadata(exportedType, false)) { - exportedDirectives.push(exportedDirMeta); - } else if (exportedPipeMeta = this.getPipeMetadata(exportedType, false)) { - exportedPipes.push(exportedPipeMeta); - } else if (exportedModuleMeta = this.getNgModuleMetadata(exportedType, false)) { - exportedModules.push(exportedModuleMeta); - } else { - throw new Error( - `Unexpected ${this._getTypeDescriptor(exportedType)} '${stringify(exportedType)}' exported by the module '${stringify(moduleType)}'`); - } - }); - } - - // Note: This will be modified later, so we rely on - // getting a new instance every time! - const transitiveModule = - this._getTransitiveNgModuleMetadata(importedModules, exportedModules); - if (meta.declarations) { - flattenAndDedupeArray(meta.declarations).forEach((declaredType) => { - if (!isValidType(declaredType)) { - throw new Error( - `Unexpected value '${stringify(declaredType)}' declared by the module '${stringify(moduleType)}'`); - } - let declaredDirMeta: cpl.CompileDirectiveMetadata; - let declaredPipeMeta: cpl.CompilePipeMetadata; - if (declaredDirMeta = this.getDirectiveMetadata(declaredType, false)) { - this._addDirectiveToModule( - declaredDirMeta, moduleType, transitiveModule, declaredDirectives, true); - } else if (declaredPipeMeta = this.getPipeMetadata(declaredType, false)) { - this._addPipeToModule( - declaredPipeMeta, moduleType, transitiveModule, declaredPipes, true); - } else { - throw new Error( - `Unexpected ${this._getTypeDescriptor(declaredType)} '${stringify(declaredType)}' declared by the module '${stringify(moduleType)}'`); - } - }); - } - - // The providers of the module have to go last - // so that they overwrite any other provider we already added. - if (meta.providers) { - providers.push(...this.getProvidersMetadata( - meta.providers, entryComponents, - `provider for the NgModule '${stringify(moduleType)}'`)); - } - - if (meta.entryComponents) { - entryComponents.push( - ...flattenAndDedupeArray(meta.entryComponents) - .map(type => this.getTypeMetadata(type, staticTypeModuleUrl(type)))); - } - - if (meta.bootstrap) { - const typeMetadata = flattenAndDedupeArray(meta.bootstrap).map(type => { - if (!isValidType(type)) { - throw new Error( - `Unexpected value '${stringify(type)}' used in the bootstrap property of module '${stringify(moduleType)}'`); - } - return this.getTypeMetadata(type, staticTypeModuleUrl(type)); - }); - bootstrapComponents.push(...typeMetadata); - } - - entryComponents.push(...bootstrapComponents); - - if (meta.schemas) { - schemas.push(...flattenAndDedupeArray(meta.schemas)); - } - - transitiveModule.entryComponents.push(...entryComponents); - transitiveModule.providers.push(...providers); - - compileMeta = new cpl.CompileNgModuleMetadata({ - type: this.getTypeMetadata(moduleType, staticTypeModuleUrl(moduleType)), - providers, - entryComponents, - bootstrapComponents, - schemas, - declaredDirectives, - exportedDirectives, - declaredPipes, - exportedPipes, - importedModules, - exportedModules, - transitiveModule, - id: meta.id, - }); - - transitiveModule.modules.push(compileMeta); - this._verifyModule(compileMeta); - this._ngModuleCache.set(moduleType, compileMeta); + if (compileMeta) { + return compileMeta; } + const meta = this._ngModuleResolver.resolve(moduleType, throwIfNotFound); + if (!meta) { + return null; + } + const declaredDirectives: cpl.CompileIdentifierMetadata[] = []; + const exportedDirectives: cpl.CompileIdentifierMetadata[] = []; + const declaredPipes: cpl.CompileIdentifierMetadata[] = []; + const exportedPipes: cpl.CompileIdentifierMetadata[] = []; + const importedModules: cpl.CompileNgModuleMetadata[] = []; + const exportedModules: cpl.CompileNgModuleMetadata[] = []; + const providers: any[] = []; + const entryComponents: cpl.CompileIdentifierMetadata[] = []; + const bootstrapComponents: cpl.CompileIdentifierMetadata[] = []; + const schemas: SchemaMetadata[] = []; + + if (meta.imports) { + flattenAndDedupeArray(meta.imports).forEach((importedType) => { + let importedModuleType: Type; + if (isValidType(importedType)) { + importedModuleType = importedType; + } else if (importedType && importedType.ngModule) { + const moduleWithProviders: ModuleWithProviders = importedType; + importedModuleType = moduleWithProviders.ngModule; + if (moduleWithProviders.providers) { + providers.push(...this._getProvidersMetadata( + moduleWithProviders.providers, entryComponents, + `provider for the NgModule '${stringify(importedModuleType)}'`)); + } + } + + if (importedModuleType) { + const importedMeta = this._loadNgModuleMetadata(importedModuleType, isSync, false); + if (importedMeta === null) { + throw new Error( + `Unexpected ${this._getTypeDescriptor(importedType)} '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`); + } + importedModules.push(importedMeta); + } else { + throw new Error( + `Unexpected value '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`); + } + }); + } + + if (meta.exports) { + flattenAndDedupeArray(meta.exports).forEach((exportedType) => { + if (!isValidType(exportedType)) { + throw new Error( + `Unexpected value '${stringify(exportedType)}' exported by the module '${stringify(moduleType)}'`); + } + let identifier = + this._getIdentifierMetadata(exportedType, staticTypeModuleUrl(exportedType)); + let exportedModuleMeta: cpl.CompileNgModuleMetadata; + if (this._directiveResolver.isDirective(exportedType)) { + exportedDirectives.push(identifier); + } else if (this._pipeResolver.isPipe(exportedType)) { + exportedPipes.push(identifier); + } else if (exportedModuleMeta = this._loadNgModuleMetadata(exportedType, isSync, false)) { + exportedModules.push(exportedModuleMeta); + } else { + throw new Error( + `Unexpected ${this._getTypeDescriptor(exportedType)} '${stringify(exportedType)}' exported by the module '${stringify(moduleType)}'`); + } + }); + } + + // Note: This will be modified later, so we rely on + // getting a new instance every time! + const transitiveModule = this._getTransitiveNgModuleMetadata(importedModules, exportedModules); + if (meta.declarations) { + flattenAndDedupeArray(meta.declarations).forEach((declaredType) => { + if (!isValidType(declaredType)) { + throw new Error( + `Unexpected value '${stringify(declaredType)}' declared by the module '${stringify(moduleType)}'`); + } + const declaredIdentifier = + this._getIdentifierMetadata(declaredType, staticTypeModuleUrl(declaredType)); + if (this._directiveResolver.isDirective(declaredType)) { + transitiveModule.directivesSet.add(declaredType); + transitiveModule.directives.push(declaredIdentifier); + declaredDirectives.push(declaredIdentifier); + this._addTypeToModule(declaredType, moduleType); + const loadingPromise = this._loadDirectiveMetadata(declaredType, isSync); + if (loadingPromise) { + transitiveModule.loadingPromises.push(loadingPromise); + } + } else if (this._pipeResolver.isPipe(declaredType)) { + transitiveModule.pipesSet.add(declaredType); + transitiveModule.pipes.push(declaredIdentifier); + declaredPipes.push(declaredIdentifier); + this._addTypeToModule(declaredType, moduleType); + this._loadPipeMetadata(declaredType); + } else { + throw new Error( + `Unexpected ${this._getTypeDescriptor(declaredType)} '${stringify(declaredType)}' declared by the module '${stringify(moduleType)}'`); + } + }); + } + + // The providers of the module have to go last + // so that they overwrite any other provider we already added. + if (meta.providers) { + providers.push(...this._getProvidersMetadata( + meta.providers, entryComponents, `provider for the NgModule '${stringify(moduleType)}'`)); + } + + if (meta.entryComponents) { + entryComponents.push( + ...flattenAndDedupeArray(meta.entryComponents) + .map(type => this._getTypeMetadata(type, staticTypeModuleUrl(type)))); + } + + if (meta.bootstrap) { + const typeMetadata = flattenAndDedupeArray(meta.bootstrap).map(type => { + if (!isValidType(type)) { + throw new Error( + `Unexpected value '${stringify(type)}' used in the bootstrap property of module '${stringify(moduleType)}'`); + } + return this._getTypeMetadata(type, staticTypeModuleUrl(type)); + }); + bootstrapComponents.push(...typeMetadata); + } + + entryComponents.push(...bootstrapComponents); + + if (meta.schemas) { + schemas.push(...flattenAndDedupeArray(meta.schemas)); + } + + transitiveModule.entryComponents.push(...entryComponents); + transitiveModule.providers.push(...providers); + + compileMeta = new cpl.CompileNgModuleMetadata({ + type: this._getTypeMetadata(moduleType, staticTypeModuleUrl(moduleType)), + providers, + entryComponents, + bootstrapComponents, + schemas, + declaredDirectives, + exportedDirectives, + declaredPipes, + exportedPipes, + importedModules, + exportedModules, + transitiveModule, + id: meta.id, + }); + + transitiveModule.modules.push(compileMeta); + this._verifyModule(compileMeta); + this._ngModuleCache.set(moduleType, compileMeta); return compileMeta; } private _verifyModule(moduleMeta: cpl.CompileNgModuleMetadata) { - moduleMeta.exportedDirectives.forEach((dirMeta) => { - if (!moduleMeta.transitiveModule.directivesSet.has(dirMeta.type.reference)) { + moduleMeta.exportedDirectives.forEach((dirIdentifier) => { + if (!moduleMeta.transitiveModule.directivesSet.has(dirIdentifier.reference)) { throw new Error( - `Can't export directive ${stringify(dirMeta.type.reference)} from ${stringify(moduleMeta.type.reference)} as it was neither declared nor imported!`); + `Can't export directive ${stringify(dirIdentifier.reference)} from ${stringify(moduleMeta.type.reference)} as it was neither declared nor imported!`); } }); - moduleMeta.exportedPipes.forEach((pipeMeta) => { - if (!moduleMeta.transitiveModule.pipesSet.has(pipeMeta.type.reference)) { + moduleMeta.exportedPipes.forEach((pipeIdentifier) => { + if (!moduleMeta.transitiveModule.pipesSet.has(pipeIdentifier.reference)) { throw new Error( - `Can't export pipe ${stringify(pipeMeta.type.reference)} from ${stringify(moduleMeta.type.reference)} as it was neither declared nor imported!`); + `Can't export pipe ${stringify(pipeIdentifier.reference)} from ${stringify(moduleMeta.type.reference)} as it was neither declared nor imported!`); } }); } private _getTypeDescriptor(type: Type): string { - if (this._directiveResolver.resolve(type, false)) { + if (this._directiveResolver.isDirective(type)) { return 'directive'; } - if (this._pipeResolver.resolve(type, false)) { + if (this._pipeResolver.isPipe(type)) { return 'pipe'; } - if (this._ngModuleResolver.resolve(type, false)) { + if (this._ngModuleResolver.isNgModule(type)) { return 'module'; } @@ -397,6 +487,7 @@ export class CompileMetadataResolver { return 'value'; } + private _addTypeToModule(type: Type, moduleType: Type) { const oldModule = this._ngModuleOfTypes.get(type); if (oldModule && oldModule !== moduleType) { @@ -421,81 +512,72 @@ export class CompileMetadataResolver { const directives = flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedDirectives)); const pipes = flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedPipes)); + const loadingPromises = ListWrapper.flatten( + transitiveExportedModules.map(ngModule => ngModule.transitiveModule.loadingPromises)); return new cpl.TransitiveCompileNgModuleMetadata( - transitiveModules, providers, entryComponents, directives, pipes); + transitiveModules, providers, entryComponents, directives, pipes, loadingPromises); } - private _addDirectiveToModule( - dirMeta: cpl.CompileDirectiveMetadata, moduleType: any, - transitiveModule: cpl.TransitiveCompileNgModuleMetadata, - declaredDirectives: cpl.CompileDirectiveMetadata[], force: boolean = false): boolean { - if (force || !transitiveModule.directivesSet.has(dirMeta.type.reference)) { - transitiveModule.directivesSet.add(dirMeta.type.reference); - transitiveModule.directives.push(dirMeta); - declaredDirectives.push(dirMeta); - this._addTypeToModule(dirMeta.type.reference, moduleType); - return true; - } - return false; - } - - private _addPipeToModule( - pipeMeta: cpl.CompilePipeMetadata, moduleType: any, - transitiveModule: cpl.TransitiveCompileNgModuleMetadata, - declaredPipes: cpl.CompilePipeMetadata[], force: boolean = false): boolean { - if (force || !transitiveModule.pipesSet.has(pipeMeta.type.reference)) { - transitiveModule.pipesSet.add(pipeMeta.type.reference); - transitiveModule.pipes.push(pipeMeta); - declaredPipes.push(pipeMeta); - this._addTypeToModule(pipeMeta.type.reference, moduleType); - return true; - } - return false; - } - - getTypeMetadata(type: Type, moduleUrl: string, dependencies: any[] = null): - cpl.CompileTypeMetadata { + private _getIdentifierMetadata(type: Type, moduleUrl: string): + cpl.CompileIdentifierMetadata { type = resolveForwardRef(type); + return new cpl.CompileIdentifierMetadata( + {name: this.sanitizeTokenName(type), moduleUrl, reference: type}); + } + + private _getTypeMetadata(type: Type, moduleUrl: string, dependencies: any[] = null): + cpl.CompileTypeMetadata { + const identifier = this._getIdentifierMetadata(type, moduleUrl); return new cpl.CompileTypeMetadata({ - name: this.sanitizeTokenName(type), - moduleUrl, - reference: type, - diDeps: this.getDependenciesMetadata(type, dependencies), - lifecycleHooks: LIFECYCLE_HOOKS_VALUES.filter(hook => hasLifecycleHook(hook, type)), + name: identifier.name, + moduleUrl: identifier.moduleUrl, + reference: identifier.reference, + diDeps: this._getDependenciesMetadata(identifier.reference, dependencies), + lifecycleHooks: + LIFECYCLE_HOOKS_VALUES.filter(hook => hasLifecycleHook(hook, identifier.reference)), }); } - getFactoryMetadata(factory: Function, moduleUrl: string, dependencies: any[] = null): + private _getFactoryMetadata(factory: Function, moduleUrl: string, dependencies: any[] = null): cpl.CompileFactoryMetadata { factory = resolveForwardRef(factory); return new cpl.CompileFactoryMetadata({ name: this.sanitizeTokenName(factory), moduleUrl, reference: factory, - diDeps: this.getDependenciesMetadata(factory, dependencies) + diDeps: this._getDependenciesMetadata(factory, dependencies) }); } - getPipeMetadata(pipeType: Type, throwIfNotFound = true): cpl.CompilePipeMetadata { - pipeType = resolveForwardRef(pipeType); - let meta = this._pipeCache.get(pipeType); - if (!meta) { - const pipeMeta = this._pipeResolver.resolve(pipeType, throwIfNotFound); - if (!pipeMeta) { - return null; - } - - meta = new cpl.CompilePipeMetadata({ - type: this.getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)), - name: pipeMeta.name, - pure: pipeMeta.pure - }); - this._pipeCache.set(pipeType, meta); + /** + * Gets the metadata for the given pipe. + * This assumes `loadNgModuleMetadata` has been called first. + */ + getPipeMetadata(pipeType: any): cpl.CompilePipeMetadata { + const pipeMeta = this._pipeCache.get(pipeType); + if (!pipeMeta) { + throw new Error( + `Illegal state: getPipeMetadata can only be called after loadNgModuleMetadata for a module that declares it. Pipe ${stringify(pipeType)}.`); } - return meta; + return pipeMeta; } - getDependenciesMetadata(typeOrFunc: Type|Function, dependencies: any[]): + private _loadPipeMetadata(pipeType: Type): void { + pipeType = resolveForwardRef(pipeType); + const pipeMeta = this._pipeResolver.resolve(pipeType); + if (!pipeMeta) { + return null; + } + + const meta = new cpl.CompilePipeMetadata({ + type: this._getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)), + name: pipeMeta.name, + pure: pipeMeta.pure + }); + this._pipeCache.set(pipeType, meta); + } + + private _getDependenciesMetadata(typeOrFunc: Type|Function, dependencies: any[]): cpl.CompileDiDependencyMetadata[] { let hasUnknownDeps = false; let params = dependencies || this._reflector.parameters(typeOrFunc) || []; @@ -540,7 +622,7 @@ export class CompileMetadataResolver { isSelf, isSkipSelf, isOptional, - token: this.getTokenMetadata(token) + token: this._getTokenMetadata(token) }); }); @@ -555,7 +637,7 @@ export class CompileMetadataResolver { return dependenciesMetadata; } - getTokenMetadata(token: any): cpl.CompileTokenMetadata { + private _getTokenMetadata(token: any): cpl.CompileTokenMetadata { token = resolveForwardRef(token); let compileToken: cpl.CompileTokenMetadata; if (typeof token === 'string') { @@ -572,8 +654,8 @@ export class CompileMetadataResolver { return compileToken; } - getProvidersMetadata( - providers: Provider[], targetEntryComponents: cpl.CompileTypeMetadata[], + private _getProvidersMetadata( + providers: Provider[], targetEntryComponents: cpl.CompileIdentifierMetadata[], debugInfo?: string): Array { const compileProviders: Array = []; providers.forEach((provider: any, providerIdx: number) => { @@ -583,9 +665,9 @@ export class CompileMetadataResolver { } let compileProvider: cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]; if (Array.isArray(provider)) { - compileProvider = this.getProvidersMetadata(provider, targetEntryComponents, debugInfo); + compileProvider = this._getProvidersMetadata(provider, targetEntryComponents, debugInfo); } else if (provider instanceof cpl.ProviderMeta) { - let tokenMeta = this.getTokenMetadata(provider.token); + let tokenMeta = this._getTokenMetadata(provider.token); if (tokenMeta.reference === resolveIdentifierToken(Identifiers.ANALYZE_FOR_ENTRY_COMPONENTS).reference) { targetEntryComponents.push(...this._getEntryComponentsFromProvider(provider)); @@ -593,7 +675,7 @@ export class CompileMetadataResolver { compileProvider = this.getProviderMetadata(provider); } } else if (isValidType(provider)) { - compileProvider = this.getTypeMetadata(provider, staticTypeModuleUrl(provider)); + compileProvider = this._getTypeMetadata(provider, staticTypeModuleUrl(provider)); } else { const providersInfo = (providers.reduce( @@ -620,8 +702,9 @@ export class CompileMetadataResolver { return compileProviders; } - private _getEntryComponentsFromProvider(provider: cpl.ProviderMeta): cpl.CompileTypeMetadata[] { - const components: cpl.CompileTypeMetadata[] = []; + private _getEntryComponentsFromProvider(provider: cpl.ProviderMeta): + cpl.CompileIdentifierMetadata[] { + const components: cpl.CompileIdentifierMetadata[] = []; const collectedIdentifiers: cpl.CompileIdentifierMetadata[] = []; if (provider.useFactory || provider.useExisting || provider.useClass) { @@ -634,9 +717,8 @@ export class CompileMetadataResolver { convertToCompileValue(provider.useValue, collectedIdentifiers); collectedIdentifiers.forEach((identifier) => { - const dirMeta = this.getDirectiveMetadata(identifier.reference, false); - if (dirMeta) { - components.push(dirMeta.type); + if (this._directiveResolver.isDirective(identifier.reference)) { + components.push(identifier); } }); return components; @@ -648,27 +730,27 @@ export class CompileMetadataResolver { let compileFactoryMetadata: cpl.CompileFactoryMetadata = null; if (provider.useClass) { - compileTypeMetadata = this.getTypeMetadata( + compileTypeMetadata = this._getTypeMetadata( provider.useClass, staticTypeModuleUrl(provider.useClass), provider.dependencies); compileDeps = compileTypeMetadata.diDeps; } else if (provider.useFactory) { - compileFactoryMetadata = this.getFactoryMetadata( + compileFactoryMetadata = this._getFactoryMetadata( provider.useFactory, staticTypeModuleUrl(provider.useFactory), provider.dependencies); compileDeps = compileFactoryMetadata.diDeps; } return new cpl.CompileProviderMetadata({ - token: this.getTokenMetadata(provider.token), + token: this._getTokenMetadata(provider.token), useClass: compileTypeMetadata, useValue: convertToCompileValue(provider.useValue, []), useFactory: compileFactoryMetadata, - useExisting: provider.useExisting ? this.getTokenMetadata(provider.useExisting) : null, + useExisting: provider.useExisting ? this._getTokenMetadata(provider.useExisting) : null, deps: compileDeps, multi: provider.multi }); } - getQueriesMetadata( + private _getQueriesMetadata( queries: {[key: string]: Query}, isViewQuery: boolean, directiveType: Type): cpl.CompileQueryMetadata[] { const res: cpl.CompileQueryMetadata[] = []; @@ -676,7 +758,7 @@ export class CompileMetadataResolver { Object.keys(queries).forEach((propertyName: string) => { const query = queries[propertyName]; if (query.isViewQuery === isViewQuery) { - res.push(this.getQueryMetadata(query, propertyName, directiveType)); + res.push(this._getQueryMetadata(query, propertyName, directiveType)); } }); @@ -685,24 +767,25 @@ export class CompileMetadataResolver { private _queryVarBindings(selector: any): string[] { return selector.split(/\s*,\s*/); } - getQueryMetadata(q: Query, propertyName: string, typeOrFunc: Type|Function): + private _getQueryMetadata(q: Query, propertyName: string, typeOrFunc: Type|Function): cpl.CompileQueryMetadata { var selectors: cpl.CompileTokenMetadata[]; if (typeof q.selector === 'string') { - selectors = this._queryVarBindings(q.selector).map(varName => this.getTokenMetadata(varName)); + selectors = + this._queryVarBindings(q.selector).map(varName => this._getTokenMetadata(varName)); } else { if (!q.selector) { throw new Error( `Can't construct a query for the property "${propertyName}" of "${stringify(typeOrFunc)}" since the query selector wasn't defined.`); } - selectors = [this.getTokenMetadata(q.selector)]; + selectors = [this._getTokenMetadata(q.selector)]; } return new cpl.CompileQueryMetadata({ selectors, first: q.first, descendants: q.descendants, propertyName, - read: q.read ? this.getTokenMetadata(q.read) : null + read: q.read ? this._getTokenMetadata(q.read) : null }); } } diff --git a/modules/@angular/compiler/src/ng_module_resolver.ts b/modules/@angular/compiler/src/ng_module_resolver.ts index 2830aca863..fce0a8a83e 100644 --- a/modules/@angular/compiler/src/ng_module_resolver.ts +++ b/modules/@angular/compiler/src/ng_module_resolver.ts @@ -22,6 +22,8 @@ function _isNgModuleMetadata(obj: any): obj is NgModule { export class NgModuleResolver { constructor(private _reflector: ReflectorReader = reflector) {} + isNgModule(type: any) { return this._reflector.annotations(type).some(_isNgModuleMetadata); } + resolve(type: Type, throwIfNotFound = true): NgModule { const ngModuleMeta: NgModule = this._reflector.annotations(type).find(_isNgModuleMetadata); diff --git a/modules/@angular/compiler/src/offline_compiler.ts b/modules/@angular/compiler/src/offline_compiler.ts index 07c9a846fe..8a2d633d88 100644 --- a/modules/@angular/compiler/src/offline_compiler.ts +++ b/modules/@angular/compiler/src/offline_compiler.ts @@ -30,28 +30,16 @@ export class SourceModule { // Returns all the source files and a mapping from modules to directives export function analyzeNgModules( programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean}, - metadataResolver: CompileMetadataResolver): { + metadataResolver: CompileMetadataResolver): Promise<{ ngModuleByPipeOrDirective: Map, files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}> -} { - const { - ngModules: programNgModules, - pipesAndDirectives: programPipesOrDirectives, - } = _extractModulesAndPipesOrDirectives(programStaticSymbols, metadataResolver); +}> { + return _loadNgModules(programStaticSymbols, options, metadataResolver).then(_analyzeNgModules); +} +function _analyzeNgModules(ngModuleMetas: CompileNgModuleMetadata[]) { const moduleMetasByRef = new Map(); - - programNgModules.forEach(modMeta => { - if (options.transitiveModules) { - // For every input modules add the list of transitively included modules - modMeta.transitiveModule.modules.forEach( - modMeta => { moduleMetasByRef.set(modMeta.type.reference, modMeta); }); - } else { - moduleMetasByRef.set(modMeta.type.reference, modMeta); - } - }); - - const ngModuleMetas = Array.from(moduleMetasByRef.values()); + ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule)); const ngModuleByPipeOrDirective = new Map(); const ngModulesByFile = new Map(); const ngDirectivesByFile = new Map(); @@ -67,31 +55,20 @@ export function analyzeNgModules( ngModulesByFile.set( srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference)); - ngModuleMeta.declaredDirectives.forEach((dirMeta: CompileDirectiveMetadata) => { - const fileUrl = dirMeta.type.reference.filePath; + ngModuleMeta.declaredDirectives.forEach((dirIdentifier) => { + const fileUrl = dirIdentifier.reference.filePath; filePaths.add(fileUrl); ngDirectivesByFile.set( - fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirMeta.type.reference)); - ngModuleByPipeOrDirective.set(dirMeta.type.reference, ngModuleMeta); + fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirIdentifier.reference)); + ngModuleByPipeOrDirective.set(dirIdentifier.reference, ngModuleMeta); }); - - ngModuleMeta.declaredPipes.forEach((pipeMeta: CompilePipeMetadata) => { - const fileUrl = pipeMeta.type.reference.filePath; + ngModuleMeta.declaredPipes.forEach((pipeIdentifier) => { + const fileUrl = pipeIdentifier.reference.filePath; filePaths.add(fileUrl); - ngModuleByPipeOrDirective.set(pipeMeta.type.reference, ngModuleMeta); + ngModuleByPipeOrDirective.set(pipeIdentifier.reference, ngModuleMeta); }); }); - // Throw an error if any of the program pipe or directives is not declared by a module - const symbolsMissingModule = - programPipesOrDirectives.filter(s => !ngModuleByPipeOrDirective.has(s)); - - if (symbolsMissingModule.length) { - const messages = symbolsMissingModule.map( - s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`); - throw new Error(messages.join('\n')); - } - const files: {srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}[] = []; filePaths.forEach((srcUrl) => { @@ -112,35 +89,29 @@ export class OfflineCompiler { private _animationCompiler = new AnimationCompiler(); constructor( - private _metadataResolver: CompileMetadataResolver, - private _directiveNormalizer: DirectiveNormalizer, private _templateParser: TemplateParser, + private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _dirWrapperCompiler: DirectiveWrapperCompiler, private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter, private _localeId: string, private _translationFormat: string, private _animationParser: AnimationParser) {} - clearCache() { - this._directiveNormalizer.clearCache(); - this._metadataResolver.clearCache(); - } + clearCache() { this._metadataResolver.clearCache(); } compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}): Promise { - const {ngModuleByPipeOrDirective, files} = - analyzeNgModules(staticSymbols, options, this._metadataResolver); - - const sourceModules = files.map( - file => this._compileSrcFile( - file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules)); - - return Promise.all(sourceModules) - .then((modules: SourceModule[][]) => ListWrapper.flatten(modules)); + return analyzeNgModules(staticSymbols, options, this._metadataResolver) + .then(({ngModuleByPipeOrDirective, files}) => { + const sourceModules = files.map( + file => this._compileSrcFile( + file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules)); + return ListWrapper.flatten(sourceModules); + }); } private _compileSrcFile( srcFileUrl: string, ngModuleByPipeOrDirective: Map, - directives: StaticSymbol[], ngModules: StaticSymbol[]): Promise { + directives: StaticSymbol[], ngModules: StaticSymbol[]): SourceModule[] { const fileSuffix = _splitTypescriptSuffix(srcFileUrl)[1]; const statements: o.Statement[] = []; const exportedVars: string[] = []; @@ -155,48 +126,38 @@ export class OfflineCompiler { (directiveType) => this._compileDirectiveWrapper(directiveType, statements))); // compile components - return Promise - .all(directives.map((dirType) => { - const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); - if (!compMeta.isComponent) { - return Promise.resolve(null); - } - const ngModule = ngModuleByPipeOrDirective.get(dirType); - if (!ngModule) { - throw new Error( - `Internal Error: cannot determine the module for component ${compMeta.type.name}!`); - } + directives.forEach((dirType) => { + const compMeta = this._metadataResolver.getDirectiveMetadata(dirType); + if (!compMeta.isComponent) { + return Promise.resolve(null); + } + const ngModule = ngModuleByPipeOrDirective.get(dirType); + if (!ngModule) { + throw new Error( + `Internal Error: cannot determine the module for component ${compMeta.type.name}!`); + } - return Promise - .all([compMeta, ...ngModule.transitiveModule.directives].map( - dirMeta => this._directiveNormalizer.normalizeDirective(dirMeta).asyncResult)) - .then((normalizedCompWithDirectives) => { - const [compMeta, ...dirMetas] = normalizedCompWithDirectives; - _assertComponent(compMeta); + _assertComponent(compMeta); - // compile styles - const stylesCompileResults = this._styleCompiler.compileComponent(compMeta); - stylesCompileResults.externalStylesheets.forEach((compiledStyleSheet) => { - outputSourceModules.push( - this._codgenStyles(srcFileUrl, compiledStyleSheet, fileSuffix)); - }); + // compile styles + const stylesCompileResults = this._styleCompiler.compileComponent(compMeta); + stylesCompileResults.externalStylesheets.forEach((compiledStyleSheet) => { + outputSourceModules.push(this._codgenStyles(srcFileUrl, compiledStyleSheet, fileSuffix)); + }); - // compile components - exportedVars.push( - this._compileComponentFactory(compMeta, fileSuffix, statements), - this._compileComponent( - compMeta, dirMetas, ngModule.transitiveModule.pipes, ngModule.schemas, - stylesCompileResults.componentStylesheet, fileSuffix, statements)); - }); - })) - .then(() => { - if (statements.length > 0) { - const srcModule = this._codegenSourceModule( - srcFileUrl, _ngfactoryModuleUrl(srcFileUrl), statements, exportedVars); - outputSourceModules.unshift(srcModule); - } - return outputSourceModules; - }); + // compile components + exportedVars.push( + this._compileComponentFactory(compMeta, ngModule, fileSuffix, statements), + this._compileComponent( + compMeta, ngModule, ngModule.transitiveModule.directives, + stylesCompileResults.componentStylesheet, fileSuffix, statements)); + }); + if (statements.length > 0) { + const srcModule = this._codegenSourceModule( + srcFileUrl, _ngfactoryModuleUrl(srcFileUrl), statements, exportedVars); + outputSourceModules.unshift(srcModule); + } + return outputSourceModules; } private _compileModule(ngModuleType: StaticSymbol, targetStatements: o.Statement[]): string { @@ -238,11 +199,11 @@ export class OfflineCompiler { } private _compileComponentFactory( - compMeta: CompileDirectiveMetadata, fileSuffix: string, + compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, fileSuffix: string, targetStatements: o.Statement[]): string { const hostMeta = createHostComponentMeta(compMeta); - const hostViewFactoryVar = - this._compileComponent(hostMeta, [compMeta], [], [], null, fileSuffix, targetStatements); + const hostViewFactoryVar = this._compileComponent( + hostMeta, ngModule, [compMeta.type], null, fileSuffix, targetStatements); const compFactoryVar = _componentFactoryName(compMeta.type); targetStatements.push( o.variable(compFactoryVar) @@ -262,12 +223,18 @@ export class OfflineCompiler { } private _compileComponent( - compMeta: CompileDirectiveMetadata, directives: CompileDirectiveMetadata[], - pipes: CompilePipeMetadata[], schemas: SchemaMetadata[], componentStyles: CompiledStylesheet, + compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, + directiveIdentifiers: CompileIdentifierMetadata[], componentStyles: CompiledStylesheet, fileSuffix: string, targetStatements: o.Statement[]): string { const parsedAnimations = this._animationParser.parseComponent(compMeta); + const directives = + directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveMetadata(dir.reference)); + const pipes = ngModule.transitiveModule.pipes.map( + pipe => this._metadataResolver.getPipeMetadata(pipe.reference)); + const parsedTemplate = this._templateParser.parse( - compMeta, compMeta.template.template, directives, pipes, schemas, compMeta.type.name); + compMeta, compMeta.template.template, directives, pipes, ngModule.schemas, + compMeta.type.name); const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); const compiledAnimations = this._animationCompiler.compile(compMeta.type.name, parsedAnimations); @@ -358,27 +325,50 @@ function _splitTypescriptSuffix(path: string): string[] { return [path, '']; } -// Group the symbols by types: -// - NgModules, -// - Pipes and Directives. -function _extractModulesAndPipesOrDirectives( - programStaticSymbols: StaticSymbol[], metadataResolver: CompileMetadataResolver) { - const ngModules: CompileNgModuleMetadata[] = []; - const pipesAndDirectives: StaticSymbol[] = []; - - programStaticSymbols.forEach(staticSymbol => { - const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false); - const directive = metadataResolver.getDirectiveMetadata(staticSymbol, false); - const pipe = metadataResolver.getPipeMetadata(staticSymbol, false); +// Load the NgModules and check +// that all directives / pipes that are present in the program +// are also declared by a module. +function _loadNgModules( + programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean}, + metadataResolver: CompileMetadataResolver): Promise { + const ngModules = new Map(); + const programPipesAndDirectives: StaticSymbol[] = []; + const ngModulePipesAndDirective = new Set(); + const loadingPromises: Promise[] = []; + const addNgModule = (staticSymbol: any) => { + if (ngModules.has(staticSymbol)) { + return false; + } + const {ngModule, loading} = metadataResolver.loadNgModuleMetadata(staticSymbol, false, false); if (ngModule) { - ngModules.push(ngModule); - } else if (directive) { - pipesAndDirectives.push(staticSymbol); - } else if (pipe) { - pipesAndDirectives.push(staticSymbol); + ngModules.set(ngModule.type.reference, ngModule); + loadingPromises.push(loading); + ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference)); + ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference)); + if (options.transitiveModules) { + // For every input modules add the list of transitively included modules + ngModule.transitiveModule.modules.forEach(modMeta => addNgModule(modMeta.type.reference)); + } + } + return !!ngModule; + }; + programStaticSymbols.forEach((staticSymbol) => { + if (!addNgModule(staticSymbol) && + (metadataResolver.isDirective(staticSymbol) || metadataResolver.isPipe(staticSymbol))) { + programPipesAndDirectives.push(staticSymbol); } }); - return {ngModules, pipesAndDirectives}; + // 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)); + + if (symbolsMissingModule.length) { + const messages = symbolsMissingModule.map( + s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`); + throw new Error(messages.join('\n')); + } + + return Promise.all(loadingPromises).then(() => Array.from(ngModules.values())); } diff --git a/modules/@angular/compiler/src/pipe_resolver.ts b/modules/@angular/compiler/src/pipe_resolver.ts index 4ca5684f9e..4b681c27ea 100644 --- a/modules/@angular/compiler/src/pipe_resolver.ts +++ b/modules/@angular/compiler/src/pipe_resolver.ts @@ -26,6 +26,11 @@ function _isPipeMetadata(type: any): boolean { export class PipeResolver { constructor(private _reflector: ReflectorReader = reflector) {} + isPipe(type: Type) { + const typeMetadata = this._reflector.annotations(resolveForwardRef(type)); + return typeMetadata && typeMetadata.some(_isPipeMetadata); + } + /** * Return {@link Pipe} for a given `Type`. */ diff --git a/modules/@angular/compiler/src/runtime_compiler.ts b/modules/@angular/compiler/src/runtime_compiler.ts index 14fafc9ae7..33f8cbd822 100644 --- a/modules/@angular/compiler/src/runtime_compiler.ts +++ b/modules/@angular/compiler/src/runtime_compiler.ts @@ -20,7 +20,6 @@ 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 {ComponentStillLoadingError} from './private_import_core'; import {CompiledStylesheet, StyleCompiler} from './style_compiler'; import {TemplateParser} from './template_parser/template_parser'; import {SyncAsyncResult} from './util'; @@ -47,9 +46,8 @@ export class RuntimeCompiler implements Compiler { constructor( private _injector: Injector, private _metadataResolver: CompileMetadataResolver, - private _templateNormalizer: DirectiveNormalizer, private _templateParser: TemplateParser, - private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, - private _ngModuleCompiler: NgModuleCompiler, + private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, + private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler, private _directiveWrapperCompiler: DirectiveWrapperCompiler, private _compilerConfig: CompilerConfig, private _animationParser: AnimationParser) {} @@ -74,38 +72,47 @@ export class RuntimeCompiler implements Compiler { private _compileModuleAndComponents(moduleType: Type, isSync: boolean): SyncAsyncResult> { - const componentPromise = this._compileComponents(moduleType, isSync); - const ngModuleFactory = this._compileModule(moduleType); - return new SyncAsyncResult(ngModuleFactory, componentPromise.then(() => ngModuleFactory)); + const loadingPromise = this._loadModules(moduleType, isSync); + const createResult = () => { + this._compileComponents(moduleType, null); + return this._compileModule(moduleType); + }; + if (isSync) { + return new SyncAsyncResult(createResult()); + } else { + return new SyncAsyncResult(null, loadingPromise.then(createResult)); + } } private _compileModuleAndAllComponents(moduleType: Type, isSync: boolean): SyncAsyncResult> { - const componentPromise = this._compileComponents(moduleType, isSync); - const ngModuleFactory = this._compileModule(moduleType); - const moduleMeta = this._metadataResolver.getNgModuleMetadata(moduleType); - const componentFactories: ComponentFactory[] = []; - const templates = new Set(); - moduleMeta.transitiveModule.modules.forEach((localModuleMeta) => { - localModuleMeta.declaredDirectives.forEach((dirMeta) => { - if (dirMeta.isComponent) { - const template = - this._createCompiledHostTemplate(dirMeta.type.reference, localModuleMeta); - templates.add(template); - componentFactories.push(template.proxyComponentFactory); - } - }); - }); - const syncResult = new ModuleWithComponentFactories(ngModuleFactory, componentFactories); - // Note: host components themselves can always be compiled synchronously as they have an - // inline template. However, we still need to wait for the components that they - // reference to be loaded / compiled. - const compile = () => { - templates.forEach((template) => { this._compileTemplate(template); }); - return syncResult; + const loadingPromise = this._loadModules(moduleType, isSync); + const createResult = () => { + const componentFactories: ComponentFactory[] = []; + this._compileComponents(moduleType, componentFactories); + return new ModuleWithComponentFactories(this._compileModule(moduleType), componentFactories); }; - const asyncResult = isSync ? Promise.resolve(compile()) : componentPromise.then(compile); - return new SyncAsyncResult(syncResult, asyncResult); + if (isSync) { + return new SyncAsyncResult(createResult()); + } else { + return new SyncAsyncResult(null, loadingPromise.then(createResult)); + } + } + + private _loadModules(mainModule: any, isSync: boolean): Promise { + var loadingPromises: Promise[] = []; + const {ngModule, loading} = this._metadataResolver.loadNgModuleMetadata(mainModule, isSync); + loadingPromises.push(loading); + // Note: the loadingPromise for a module only includes the loading of the exported directives + // of imported modules. + // However, for runtime compilation, we want to transitively compile all modules, + // so we also need to call loadNgModuleMetadata for all nested modules. + ngModule.transitiveModule.modules.forEach((localModuleMeta) => { + loadingPromises.push( + this._metadataResolver.loadNgModuleMetadata(localModuleMeta.type.reference, isSync) + .loading); + }); + return Promise.all(loadingPromises); } private _compileModule(moduleType: Type): NgModuleFactory { @@ -137,23 +144,30 @@ export class RuntimeCompiler implements Compiler { /** * @internal */ - _compileComponents(mainModule: Type, isSync: boolean): Promise { - const templates = new Set(); - var loadingPromises: Promise[] = []; - + _compileComponents(mainModule: Type, allComponentFactories: ComponentFactory[]) { const ngModule = this._metadataResolver.getNgModuleMetadata(mainModule); const moduleByDirective = new Map(); + const templates = new Set(); + ngModule.transitiveModule.modules.forEach((localModuleMeta) => { - localModuleMeta.declaredDirectives.forEach((dirMeta) => { - moduleByDirective.set(dirMeta.type.reference, localModuleMeta); + localModuleMeta.declaredDirectives.forEach((dirIdentifier) => { + moduleByDirective.set(dirIdentifier.reference, localModuleMeta); + const dirMeta = this._metadataResolver.getDirectiveMetadata(dirIdentifier.reference); this._compileDirectiveWrapper(dirMeta, localModuleMeta); if (dirMeta.isComponent) { templates.add(this._createCompiledTemplate(dirMeta, localModuleMeta)); + if (allComponentFactories) { + const template = + this._createCompiledHostTemplate(dirMeta.type.reference, localModuleMeta); + templates.add(template); + allComponentFactories.push(template.proxyComponentFactory); + } } }); }); ngModule.transitiveModule.modules.forEach((localModuleMeta) => { - localModuleMeta.declaredDirectives.forEach((dirMeta) => { + localModuleMeta.declaredDirectives.forEach((dirIdentifier) => { + const dirMeta = this._metadataResolver.getDirectiveMetadata(dirIdentifier.reference); if (dirMeta.isComponent) { dirMeta.entryComponents.forEach((entryComponentType) => { const moduleMeta = moduleByDirective.get(entryComponentType.reference); @@ -167,24 +181,7 @@ export class RuntimeCompiler implements Compiler { templates.add(this._createCompiledHostTemplate(entryComponentType.reference, moduleMeta)); }); }); - - templates.forEach((template) => { - if (template.loading) { - if (isSync) { - throw new ComponentStillLoadingError(template.compType.reference); - } else { - loadingPromises.push(template.loading); - } - } - }); - const compile = - () => { templates.forEach((template) => { this._compileTemplate(template); }); }; - if (isSync) { - compile(); - return Promise.resolve(null); - } else { - return Promise.all(loadingPromises).then(compile); - } + templates.forEach((template) => this._compileTemplate(template)); } clearCacheFor(type: Type) { @@ -193,7 +190,6 @@ export class RuntimeCompiler implements Compiler { this._compiledHostTemplateCache.delete(type); var compiledTemplate = this._compiledTemplateCache.get(type); if (compiledTemplate) { - this._templateNormalizer.clearCacheFor(compiledTemplate.normalizedCompMeta); this._compiledTemplateCache.delete(type); } } @@ -202,7 +198,6 @@ export class RuntimeCompiler implements Compiler { this._metadataResolver.clearCache(); this._compiledTemplateCache.clear(); this._compiledHostTemplateCache.clear(); - this._templateNormalizer.clearCache(); this._compiledNgModuleCache.clear(); } @@ -218,8 +213,7 @@ export class RuntimeCompiler implements Compiler { assertComponent(compMeta); var hostMeta = createHostComponentMeta(compMeta); compiledTemplate = new CompiledTemplate( - true, compMeta.selector, compMeta.type, ngModule, [compMeta], - this._templateNormalizer.normalizeDirective(hostMeta)); + true, compMeta.selector, compMeta.type, hostMeta, ngModule, [compMeta.type]); this._compiledHostTemplateCache.set(compType, compiledTemplate); } return compiledTemplate; @@ -231,8 +225,8 @@ export class RuntimeCompiler implements Compiler { if (!compiledTemplate) { assertComponent(compMeta); compiledTemplate = new CompiledTemplate( - false, compMeta.selector, compMeta.type, ngModule, ngModule.transitiveModule.directives, - this._templateNormalizer.normalizeDirective(compMeta)); + false, compMeta.selector, compMeta.type, compMeta, ngModule, + ngModule.transitiveModule.directives); this._compiledTemplateCache.set(compMeta.type.reference, compiledTemplate); } return compiledTemplate; @@ -243,16 +237,7 @@ export class RuntimeCompiler implements Compiler { this._compiledTemplateCache.get(compType); if (!compiledTemplate) { throw new Error( - `Illegal state: Compiled view for component ${stringify(compType)} does not exist!`); - } - return compiledTemplate; - } - - private _assertComponentLoaded(compType: any, isHost: boolean): CompiledTemplate { - const compiledTemplate = this._assertComponentKnown(compType, isHost); - if (compiledTemplate.loading) { - throw new Error( - `Illegal state: CompiledTemplate for ${stringify(compType)} (isHost: ${isHost}) is still loading!`); + `Illegal state: Compiled view for component ${stringify(compType)} (host: ${isHost}) does not exist!`); } return compiledTemplate; } @@ -285,34 +270,36 @@ export class RuntimeCompiler implements Compiler { if (template.isCompiled) { return; } - const compMeta = template.normalizedCompMeta; + const compMeta = template.compMeta; const externalStylesheetsByModuleUrl = new Map(); const stylesCompileResult = this._styleCompiler.compileComponent(compMeta); stylesCompileResult.externalStylesheets.forEach( (r) => { externalStylesheetsByModuleUrl.set(r.meta.moduleUrl, r); }); this._resolveStylesCompileResult( stylesCompileResult.componentStylesheet, externalStylesheetsByModuleUrl); - const viewCompMetas = template.viewComponentTypes.map( - (compType) => this._assertComponentLoaded(compType, false).normalizedCompMeta); const parsedAnimations = this._animationParser.parseComponent(compMeta); + const directives = + template.directives.map(dir => this._metadataResolver.getDirectiveMetadata(dir.reference)); + const pipes = template.ngModule.transitiveModule.pipes.map( + pipe => this._metadataResolver.getPipeMetadata(pipe.reference)); const parsedTemplate = this._templateParser.parse( - compMeta, compMeta.template.template, template.viewDirectives.concat(viewCompMetas), - template.viewPipes, template.schemas, compMeta.type.name); + compMeta, compMeta.template.template, directives, pipes, template.ngModule.schemas, + compMeta.type.name); const compiledAnimations = this._animationCompiler.compile(compMeta.type.name, parsedAnimations); const compileResult = this._viewCompiler.compileComponent( compMeta, parsedTemplate, ir.variable(stylesCompileResult.componentStylesheet.stylesVar), - template.viewPipes, compiledAnimations); + pipes, compiledAnimations); compileResult.dependencies.forEach((dep) => { let depTemplate: CompiledTemplate; if (dep instanceof ViewClassDependency) { let vfd = dep; - depTemplate = this._assertComponentLoaded(vfd.comp.reference, false); + depTemplate = this._assertComponentKnown(vfd.comp.reference, false); vfd.placeholder.reference = depTemplate.proxyViewClass; vfd.placeholder.name = `View_${vfd.comp.name}`; } else if (dep instanceof ComponentFactoryDependency) { let cfd = dep; - depTemplate = this._assertComponentLoaded(cfd.comp.reference, true); + depTemplate = this._assertComponentKnown(cfd.comp.reference, true); cfd.placeholder.reference = depTemplate.proxyComponentFactory; cfd.placeholder.name = `compFactory_${cfd.comp.name}`; } else if (dep instanceof DirectiveWrapperDependency) { @@ -361,29 +348,12 @@ class CompiledTemplate { private _viewClass: Function = null; proxyViewClass: Type; proxyComponentFactory: ComponentFactory; - loading: Promise = null; - private _normalizedCompMeta: CompileDirectiveMetadata = null; isCompiled = false; - isCompiledWithDeps = false; - viewComponentTypes: Type[] = []; - viewDirectives: CompileDirectiveMetadata[] = []; - viewPipes: CompilePipeMetadata[]; - schemas: SchemaMetadata[]; constructor( public isHost: boolean, selector: string, public compType: CompileIdentifierMetadata, - public ngModule: CompileNgModuleMetadata, - viewDirectiveAndComponents: CompileDirectiveMetadata[], - _normalizeResult: SyncAsyncResult) { - this.viewPipes = ngModule.transitiveModule.pipes; - this.schemas = ngModule.schemas; - viewDirectiveAndComponents.forEach((dirMeta) => { - if (dirMeta.isComponent) { - this.viewComponentTypes.push(dirMeta.type.reference); - } else { - this.viewDirectives.push(dirMeta); - } - }); + public compMeta: CompileDirectiveMetadata, public ngModule: CompileNgModuleMetadata, + public directives: CompileIdentifierMetadata[]) { const self = this; this.proxyViewClass = function() { if (!self._viewClass) { @@ -395,21 +365,6 @@ class CompiledTemplate { this.proxyComponentFactory = isHost ? new ComponentFactory(selector, this.proxyViewClass, compType.reference) : null; - if (_normalizeResult.syncResult) { - this._normalizedCompMeta = _normalizeResult.syncResult; - } else { - this.loading = _normalizeResult.asyncResult.then((normalizedCompMeta) => { - this._normalizedCompMeta = normalizedCompMeta; - this.loading = null; - }); - } - } - - get normalizedCompMeta(): CompileDirectiveMetadata { - if (this.loading) { - throw new Error(`Template is still loading for ${this.compType.name}!`); - } - return this._normalizedCompMeta; } compiled(viewClass: Function) { @@ -417,8 +372,6 @@ class CompiledTemplate { this.proxyViewClass.prototype = viewClass.prototype; this.isCompiled = true; } - - depsCompiled() { this.isCompiledWithDeps = true; } } function assertComponent(meta: CompileDirectiveMetadata) { diff --git a/modules/@angular/compiler/test/directive_normalizer_spec.ts b/modules/@angular/compiler/test/directive_normalizer_spec.ts index 74c58465d0..adb4f19efa 100644 --- a/modules/@angular/compiler/test/directive_normalizer_spec.ts +++ b/modules/@angular/compiler/test/directive_normalizer_spec.ts @@ -18,79 +18,78 @@ import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@ang import {SpyResourceLoader} from './spies'; +const SOME_MODULE_URL = 'package:some/module/a.js'; +const SOME_HTTP_MODULE_URL = 'http://some/module/a.js'; + export function main() { describe('DirectiveNormalizer', () => { - var dirType: CompileTypeMetadata; - var dirTypeWithHttpUrl: CompileTypeMetadata; - beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); }); - beforeEach(() => { - dirType = new CompileTypeMetadata({moduleUrl: 'package:some/module/a.js', name: 'SomeComp'}); - dirTypeWithHttpUrl = - new CompileTypeMetadata({moduleUrl: 'http://some/module/a.js', name: 'SomeComp'}); - }); - describe('normalizeDirective', () => { it('should throw if no template was specified', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - expect(() => normalizer.normalizeDirective(new CompileDirectiveMetadata({ - type: dirType, - isComponent: true, - template: - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}) - }))).toThrowError('No template specified for component SomeComp'); + expect(() => normalizer.normalizeTemplate({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + })).toThrowError('No template specified for component SomeComp'); })); }); describe('normalizeTemplateSync', () => { it('should store the template', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - let template = normalizer.normalizeTemplateSync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: 'a', - templateUrl: null, - styles: [], - styleUrls: [] - })); + let template = normalizer.normalizeTemplateSync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: 'a', + templateUrl: null, + styles: [], + styleUrls: [] + }); expect(template.template).toEqual('a'); expect(template.templateUrl).toEqual('package:some/module/a.js'); })); it('should resolve styles on the annotation against the moduleUrl', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - let template = normalizer.normalizeTemplateSync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })); + let template = normalizer.normalizeTemplateSync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: '', + templateUrl: null, + styles: [], + styleUrls: ['test.css'] + }); expect(template.styleUrls).toEqual(['package:some/module/test.css']); })); it('should resolve styles in the template against the moduleUrl', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - let template = - normalizer.normalizeTemplateSync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: [] - })); + let template = normalizer.normalizeTemplateSync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: '', + templateUrl: null, + styles: [], + styleUrls: [] + }); expect(template.styleUrls).toEqual(['package:some/module/test.css']); })); it('should use ViewEncapsulation.Emulated by default', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - let template = normalizer.normalizeTemplateSync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })); + let template = normalizer.normalizeTemplateSync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: '', + templateUrl: null, + styles: [], + styleUrls: ['test.css'] + }); expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated); })); @@ -99,14 +98,15 @@ export function main() { [CompilerConfig, DirectiveNormalizer], (config: CompilerConfig, normalizer: DirectiveNormalizer) => { config.defaultEncapsulation = ViewEncapsulation.None; - let template = - normalizer.normalizeTemplateSync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })); + let template = normalizer.normalizeTemplateSync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: '', + templateUrl: null, + styles: [], + styleUrls: ['test.css'] + }); expect(template.encapsulation).toEqual(ViewEncapsulation.None); })); }); @@ -120,13 +120,15 @@ export function main() { resourceLoader: MockResourceLoader) => { resourceLoader.expect('package:some/module/sometplurl.html', 'a'); normalizer - .normalizeTemplateAsync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: null, - templateUrl: 'sometplurl.html', - styles: [], - styleUrls: ['test.css'] - })) + .normalizeTemplateAsync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: null, + templateUrl: 'sometplurl.html', + styles: [], + styleUrls: ['test.css'] + }) .then((template: CompileTemplateMetadata) => { expect(template.template).toEqual('a'); expect(template.templateUrl).toEqual('package:some/module/sometplurl.html'); @@ -142,13 +144,15 @@ export function main() { resourceLoader: MockResourceLoader) => { resourceLoader.expect('package:some/module/tpl/sometplurl.html', ''); normalizer - .normalizeTemplateAsync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: null, - templateUrl: 'tpl/sometplurl.html', - styles: [], - styleUrls: ['test.css'] - })) + .normalizeTemplateAsync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: null, + templateUrl: 'tpl/sometplurl.html', + styles: [], + styleUrls: ['test.css'] + }) .then((template: CompileTemplateMetadata) => { expect(template.styleUrls).toEqual(['package:some/module/test.css']); async.done(); @@ -164,13 +168,15 @@ export function main() { resourceLoader.expect( 'package:some/module/tpl/sometplurl.html', ''); normalizer - .normalizeTemplateAsync(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: null, - templateUrl: 'tpl/sometplurl.html', - styles: [], - styleUrls: [] - })) + .normalizeTemplateAsync({ + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + template: null, + templateUrl: 'tpl/sometplurl.html', + styles: [], + styleUrls: [] + }) .then((template: CompileTemplateMetadata) => { expect(template.styleUrls).toEqual(['package:some/module/tpl/test.css']); async.done(); @@ -249,13 +255,15 @@ export function main() { (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, resourceLoader: MockResourceLoader) => { resourceLoader.expect('package:some/module/cmp.html', 'a'); - var templateMeta = new CompileTemplateMetadata({ + var prenormMeta = { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, templateUrl: 'cmp.html', - }); + }; Promise .all([ - normalizer.normalizeTemplateAsync(dirType, templateMeta), - normalizer.normalizeTemplateAsync(dirType, templateMeta) + normalizer.normalizeTemplateAsync(prenormMeta), + normalizer.normalizeTemplateAsync(prenormMeta) ]) .then((templates: CompileTemplateMetadata[]) => { expect(templates[0].template).toEqual('a'); @@ -273,8 +281,13 @@ export function main() { var viewEncapsulation = ViewEncapsulation.Native; var template = normalizer.normalizeLoadedTemplate( - dirType, new CompileTemplateMetadata( - {encapsulation: viewEncapsulation, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: viewEncapsulation, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.encapsulation).toBe(viewEncapsulation); })); @@ -282,17 +295,27 @@ export function main() { it('should keep the template as html', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), 'a', - 'package:some/module/'); + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, + 'a', 'package:some/module/'); expect(template.template).toEqual('a'); })); it('should collect ngContent', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.ngContentSelectors).toEqual(['a']); })); @@ -300,8 +323,13 @@ export function main() { it('should normalize ngContent wildcard selector', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.ngContentSelectors).toEqual(['*', '*', '*']); @@ -310,8 +338,13 @@ export function main() { it('should collect top level styles in the template', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.styles).toEqual(['a']); })); @@ -319,8 +352,13 @@ export function main() { it('should collect styles inside in elements', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '
', 'package:some/module/'); expect(template.styles).toEqual(['a']); })); @@ -328,8 +366,13 @@ export function main() { it('should collect styleUrls in the template', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.styleUrls).toEqual(['package:some/module/aUrl']); })); @@ -337,8 +380,13 @@ export function main() { it('should collect styleUrls in elements', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '
', 'package:some/module/'); expect(template.styleUrls).toEqual(['package:some/module/aUrl']); })); @@ -346,8 +394,13 @@ export function main() { it('should ignore link elements with non stylesheet rel attribute', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.styleUrls).toEqual([]); })); @@ -355,8 +408,13 @@ export function main() { it('should ignore link elements with absolute urls but non package: scheme', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '', 'package:some/module/'); expect(template.styleUrls).toEqual([]); })); @@ -364,8 +422,13 @@ export function main() { it('should extract @import style urls into styleAbsUrl', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, new CompileTemplateMetadata( - {encapsulation: null, styles: ['@import "test.css";'], styleUrls: []}), + { + 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']); @@ -374,11 +437,13 @@ export function main() { it('should not resolve relative urls in inline styles', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, new CompileTemplateMetadata({ + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, encapsulation: null, styles: ['.foo{background-image: url(\'double.jpg\');'], styleUrls: [] - }), + }, '', 'package:some/module/id'); expect(template.styles).toEqual(['.foo{background-image: url(\'double.jpg\');']); })); @@ -386,8 +451,13 @@ export function main() { it('should resolve relative style urls in styleUrls', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, new CompileTemplateMetadata( - {encapsulation: null, styles: [], styleUrls: ['test.css']}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: ['test.css'] + }, '', 'package:some/module/id'); expect(template.styles).toEqual([]); expect(template.styleUrls).toEqual(['package:some/module/test.css']); @@ -396,8 +466,13 @@ export function main() { it('should resolve relative style urls in styleUrls with http directive url', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirTypeWithHttpUrl, new CompileTemplateMetadata( - {encapsulation: null, styles: [], styleUrls: ['test.css']}), + { + componentType: SomeComp, + moduleUrl: SOME_HTTP_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: ['test.css'] + }, '', 'http://some/module/id'); expect(template.styles).toEqual([]); expect(template.styleUrls).toEqual(['http://some/module/test.css']); @@ -406,8 +481,13 @@ export function main() { it('should normalize ViewEncapsulation.Emulated to ViewEncapsulation.None if there are no styles nor stylesheets', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, new CompileTemplateMetadata( - {encapsulation: ViewEncapsulation.Emulated, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: ViewEncapsulation.Emulated, + styles: [], + styleUrls: [] + }, '', 'package:some/module/id'); expect(template.encapsulation).toEqual(ViewEncapsulation.None); })); @@ -415,8 +495,13 @@ export function main() { it('should ignore ng-content in elements with ngNonBindable', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { var template = normalizer.normalizeLoadedTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}), + { + componentType: SomeComp, + moduleUrl: SOME_MODULE_URL, + encapsulation: null, + styles: [], + styleUrls: [] + }, '
', 'package:some/module/'); expect(template.ngContentSelectors).toEqual([]); @@ -425,8 +510,13 @@ export function main() { it('should still collect ', 'package:some/module/'); expect(template.styles).toEqual(['div {color:red}']); })); @@ -444,3 +534,5 @@ function programResourceLoaderSpy(spy: SpyResourceLoader, results: {[key: string } }); } + +class SomeComp {} \ No newline at end of file diff --git a/modules/@angular/compiler/test/metadata_resolver_spec.ts b/modules/@angular/compiler/test/metadata_resolver_spec.ts index b234e10f28..c4ed221a64 100644 --- a/modules/@angular/compiler/test/metadata_resolver_spec.ts +++ b/modules/@angular/compiler/test/metadata_resolver_spec.ts @@ -9,10 +9,12 @@ import {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings'; import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, Component, DoCheck, Injectable, NgModule, OnChanges, OnDestroy, OnInit, Pipe, SimpleChanges, ViewEncapsulation} from '@angular/core'; import {LIFECYCLE_HOOKS_VALUES} from '@angular/core/src/metadata/lifecycle_hooks'; -import {TestBed, inject} from '@angular/core/testing'; +import {TestBed, async, inject} from '@angular/core/testing'; import {stringify} from '../src/facade/lang'; import {CompileMetadataResolver} from '../src/metadata_resolver'; +import {ResourceLoader} from '../src/resource_loader'; +import {MockResourceLoader} from '../testing/resource_loader_mock'; import {MalformedStylesComponent} from './metadata_resolver_fixture'; @@ -20,186 +22,305 @@ export function main() { describe('CompileMetadataResolver', () => { beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); }); - describe('getDirectiveMetadata', () => { - it('should read metadata', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - const meta = resolver.getDirectiveMetadata(ComponentWithEverything); - expect(meta.selector).toEqual('someSelector'); - expect(meta.exportAs).toEqual('someExportAs'); - expect(meta.isComponent).toBe(true); - expect(meta.type.reference).toBe(ComponentWithEverything); - expect(meta.type.name).toEqual(stringify(ComponentWithEverything)); - expect(meta.type.lifecycleHooks).toEqual(LIFECYCLE_HOOKS_VALUES); - expect(meta.changeDetection).toBe(ChangeDetectionStrategy.Default); - expect(meta.inputs).toEqual({'someProp': 'someProp'}); - expect(meta.outputs).toEqual({'someEvent': 'someEvent'}); - expect(meta.hostListeners).toEqual({'someHostListener': 'someHostListenerExpr'}); - expect(meta.hostProperties).toEqual({'someHostProp': 'someHostPropExpr'}); - expect(meta.hostAttributes).toEqual({'someHostAttr': 'someHostAttrValue'}); - expect(meta.template.encapsulation).toBe(ViewEncapsulation.Emulated); - expect(meta.template.styles).toEqual(['someStyle']); - expect(meta.template.styleUrls).toEqual(['someStyleUrl']); - expect(meta.template.template).toEqual('someTemplate'); - expect(meta.template.templateUrl).toEqual('someTemplateUrl'); - expect(meta.template.interpolation).toEqual(['{{', '}}']); - })); - - it('should use the moduleUrl from the reflector if none is given', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - const value: string = - resolver.getDirectiveMetadata(ComponentWithoutModuleId).type.moduleUrl; - const expectedEndValue = './ComponentWithoutModuleId'; - expect(value.endsWith(expectedEndValue)).toBe(true); - })); - - it('should throw when the moduleId is not a string', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidModuleId)) - .toThrowError( - `moduleId should be a string in "ComponentWithInvalidModuleId". See` + - ` https://goo.gl/wIDDiL for more information.\n` + - `If you're using Webpack you should inline the template and the styles, see` + - ` https://goo.gl/X2J8zc.`); - })); - - - it('should throw when metadata is incorrectly typed', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(MalformedStylesComponent)) - .toThrowError(`Expected 'styles' to be an array of strings.`); - })); - - it('should throw with descriptive error message when provider token can not be resolved', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(MyBrokenComp1)) - .toThrowError(`Can't resolve all parameters for MyBrokenComp1: (?).`); - })); - it('should throw with descriptive error message when a directive is passed to imports', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - @NgModule({imports: [ComponentWithoutModuleId]}) - class ModuleWithImportedComponent { - } - expect(() => resolver.getNgModuleMetadata(ModuleWithImportedComponent)) - .toThrowError( - `Unexpected directive 'ComponentWithoutModuleId' imported by the module 'ModuleWithImportedComponent'`); - })); - - it('should throw with descriptive error message when a pipe is passed to imports', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - @Pipe({name: 'somePipe'}) - class SomePipe { - } - @NgModule({imports: [SomePipe]}) - class ModuleWithImportedPipe { - } - expect(() => resolver.getNgModuleMetadata(ModuleWithImportedPipe)) - .toThrowError( - `Unexpected pipe 'SomePipe' imported by the module 'ModuleWithImportedPipe'`); - })); - - it('should throw with descriptive error message when a module is passed to declarations', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - @NgModule({}) - class SomeModule { - } - @NgModule({declarations: [SomeModule]}) - class ModuleWithDeclaredModule { - } - expect(() => resolver.getNgModuleMetadata(ModuleWithDeclaredModule)) - .toThrowError( - `Unexpected module 'SomeModule' declared by the module 'ModuleWithDeclaredModule'`); - })); - - it('should throw with descriptive error message when null is passed to declarations', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - @NgModule({declarations: [null]}) - class ModuleWithNullDeclared { - } - expect(() => resolver.getNgModuleMetadata(ModuleWithNullDeclared)) - .toThrowError( - `Unexpected value 'null' declared by the module 'ModuleWithNullDeclared'`); - })); - - it('should throw with descriptive error message when null is passed to imports', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - @NgModule({imports: [null]}) - class ModuleWithNullImported { - } - expect(() => resolver.getNgModuleMetadata(ModuleWithNullImported)) - .toThrowError( - `Unexpected value 'null' imported by the module 'ModuleWithNullImported'`); - })); - - - it('should throw with descriptive error message when a param token of a dependency is undefined', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(MyBrokenComp2)) - .toThrowError(`Can't resolve all parameters for NonAnnotatedService: (?).`); - })); - - it('should throw with descriptive error message when one of providers is not present', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(MyBrokenComp3)) - .toThrowError( - `Invalid providers for "MyBrokenComp3" - only instances of Provider and Type are allowed, got: [SimpleService, ?null?, ...]`); - })); - - it('should throw with descriptive error message when one of viewProviders is not present', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(MyBrokenComp4)) - .toThrowError( - `Invalid viewProviders for "MyBrokenComp4" - only instances of Provider and Type are allowed, got: [?null?, ...]`); - })); - - it('should throw with descriptive error message when null or undefined is passed to module bootstrap', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - @NgModule({bootstrap: [null]}) - class ModuleWithNullBootstrap { - } - @NgModule({bootstrap: [undefined]}) - class ModuleWithUndefinedBootstrap { - } - - expect(() => resolver.getNgModuleMetadata(ModuleWithNullBootstrap)) - .toThrowError( - `Unexpected value 'null' used in the bootstrap property of module 'ModuleWithNullBootstrap'`); - expect(() => resolver.getNgModuleMetadata(ModuleWithUndefinedBootstrap)) - .toThrowError( - `Unexpected value 'undefined' used in the bootstrap property of module 'ModuleWithUndefinedBootstrap'`); - })); - - it('should throw an error when the interpolation config has invalid symbols', - inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation1)) - .toThrowError(`[' ', ' '] contains unusable interpolation symbol.`); - expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation2)) - .toThrowError(`['{', '}'] contains unusable interpolation symbol.`); - expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation3)) - .toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`); - expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation4)) - .toThrowError(`['&#', '}}'] contains unusable interpolation symbol.`); - expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation5)) - .toThrowError(`['{', '}}'] contains unusable interpolation symbol.`); - })); - }); - - it('should dedupe declarations in NgModule', + it('should throw on the get... methods if the module has not been loaded yet', inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { - - @Component({template: ''}) - class MyComp { + @NgModule({}) + class SomeModule { } - @NgModule({declarations: [MyComp, MyComp]}) - class MyModule { + @Pipe({name: 'pipe'}) + class SomePipe { } - const modMeta = resolver.getNgModuleMetadata(MyModule); - expect(modMeta.declaredDirectives.length).toBe(1); - expect(modMeta.declaredDirectives[0].type.reference).toBe(MyComp); + expect(() => resolver.getNgModuleMetadata(SomeModule)).toThrowError(/Illegal state/); + expect(() => resolver.getDirectiveMetadata(ComponentWithEverythingInline)) + .toThrowError(/Illegal state/); + expect(() => resolver.getPipeMetadata(SomePipe)).toThrowError(/Illegal state/); })); + it('should read metadata in sync for components with inline resources', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [ComponentWithEverythingInline]}) + class SomeModule { + } + resolver.loadNgModuleMetadata(SomeModule, true); + + const meta = resolver.getDirectiveMetadata(ComponentWithEverythingInline); + expect(meta.selector).toEqual('someSelector'); + expect(meta.exportAs).toEqual('someExportAs'); + expect(meta.isComponent).toBe(true); + expect(meta.type.reference).toBe(ComponentWithEverythingInline); + expect(meta.type.name).toEqual(stringify(ComponentWithEverythingInline)); + expect(meta.type.lifecycleHooks).toEqual(LIFECYCLE_HOOKS_VALUES); + expect(meta.changeDetection).toBe(ChangeDetectionStrategy.Default); + expect(meta.inputs).toEqual({'someProp': 'someProp'}); + expect(meta.outputs).toEqual({'someEvent': 'someEvent'}); + expect(meta.hostListeners).toEqual({'someHostListener': 'someHostListenerExpr'}); + expect(meta.hostProperties).toEqual({'someHostProp': 'someHostPropExpr'}); + expect(meta.hostAttributes).toEqual({'someHostAttr': 'someHostAttrValue'}); + expect(meta.template.encapsulation).toBe(ViewEncapsulation.Emulated); + expect(meta.template.styles).toEqual(['someStyle']); + expect(meta.template.template).toEqual('someTemplate'); + expect(meta.template.interpolation).toEqual(['{{', '}}']); + })); + + it('should throw when reading metadata for component with external resources when sync=true is passed', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [ComponentWithExternalResources]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError( + `Can't compile synchronously as ${stringify(ComponentWithExternalResources)} is still being loaded!`); + })); + + it('should read external metadata when sync=false', + async(inject( + [CompileMetadataResolver, ResourceLoader], + (resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => { + @NgModule({declarations: [ComponentWithExternalResources]}) + class SomeModule { + } + + resourceLoader.when('someTemplateUrl', 'someTemplate'); + resolver.loadNgModuleMetadata(SomeModule, false).loading.then(() => { + const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources); + expect(meta.selector).toEqual('someSelector'); + expect(meta.template.styleUrls).toEqual(['someStyleUrl']); + expect(meta.template.templateUrl).toEqual('someTemplateUrl'); + expect(meta.template.template).toEqual('someTemplate'); + }); + resourceLoader.flush(); + }))); + + it('should wait for external resources of imported modules', + async(inject( + [CompileMetadataResolver, ResourceLoader], + (resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => { + @NgModule({declarations: [ComponentWithExternalResources]}) + class SomeImportedModule { + } + + @NgModule({imports: [SomeImportedModule]}) + class SomeModule { + } + + resourceLoader.when('someTemplateUrl', 'someTemplate'); + resolver.loadNgModuleMetadata(SomeModule, false).loading.then(() => { + const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources); + expect(meta.selector).toEqual('someSelector'); + }); + resourceLoader.flush(); + }))); + + it('should use the moduleUrl from the reflector if none is given', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [ComponentWithoutModuleId]}) + class SomeModule { + } + resolver.loadNgModuleMetadata(SomeModule, true); + + const value: string = + resolver.getDirectiveMetadata(ComponentWithoutModuleId).type.moduleUrl; + const expectedEndValue = './ComponentWithoutModuleId'; + expect(value.endsWith(expectedEndValue)).toBe(true); + })); + + it('should throw when the moduleId is not a string', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [ComponentWithInvalidModuleId]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError( + `moduleId should be a string in "ComponentWithInvalidModuleId". See` + + ` https://goo.gl/wIDDiL for more information.\n` + + `If you're using Webpack you should inline the template and the styles, see` + + ` https://goo.gl/X2J8zc.`); + })); + + + it('should throw when metadata is incorrectly typed', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [MalformedStylesComponent]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError(`Expected 'styles' to be an array of strings.`); + })); + + it('should throw with descriptive error message when provider token can not be resolved', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [MyBrokenComp1]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError(`Can't resolve all parameters for MyBrokenComp1: (?).`); + })); + it('should throw with descriptive error message when a directive is passed to imports', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({imports: [ComponentWithoutModuleId]}) + class ModuleWithImportedComponent { + } + expect(() => resolver.loadNgModuleMetadata(ModuleWithImportedComponent, true)) + .toThrowError( + `Unexpected directive 'ComponentWithoutModuleId' imported by the module 'ModuleWithImportedComponent'`); + })); + + it('should throw with descriptive error message when a pipe is passed to imports', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @Pipe({name: 'somePipe'}) + class SomePipe { + } + @NgModule({imports: [SomePipe]}) + class ModuleWithImportedPipe { + } + expect(() => resolver.loadNgModuleMetadata(ModuleWithImportedPipe, true)) + .toThrowError( + `Unexpected pipe 'SomePipe' imported by the module 'ModuleWithImportedPipe'`); + })); + + it('should throw with descriptive error message when a module is passed to declarations', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({}) + class SomeModule { + } + @NgModule({declarations: [SomeModule]}) + class ModuleWithDeclaredModule { + } + expect(() => resolver.loadNgModuleMetadata(ModuleWithDeclaredModule, true)) + .toThrowError( + `Unexpected module 'SomeModule' declared by the module 'ModuleWithDeclaredModule'`); + })); + + it('should throw with descriptive error message when null is passed to declarations', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [null]}) + class ModuleWithNullDeclared { + } + expect(() => resolver.loadNgModuleMetadata(ModuleWithNullDeclared, true)) + .toThrowError( + `Unexpected value 'null' declared by the module 'ModuleWithNullDeclared'`); + })); + + it('should throw with descriptive error message when null is passed to imports', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({imports: [null]}) + class ModuleWithNullImported { + } + expect(() => resolver.loadNgModuleMetadata(ModuleWithNullImported, true)) + .toThrowError( + `Unexpected value 'null' imported by the module 'ModuleWithNullImported'`); + })); + + + it('should throw with descriptive error message when a param token of a dependency is undefined', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [MyBrokenComp2]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError(`Can't resolve all parameters for NonAnnotatedService: (?).`); + })); + + it('should throw with descriptive error message when one of providers is not present', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [MyBrokenComp3]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError( + `Invalid providers for "MyBrokenComp3" - only instances of Provider and Type are allowed, got: [SimpleService, ?null?, ...]`); + })); + + it('should throw with descriptive error message when one of viewProviders is not present', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [MyBrokenComp4]}) + class SomeModule { + } + + expect(() => resolver.loadNgModuleMetadata(SomeModule, true)) + .toThrowError( + `Invalid viewProviders for "MyBrokenComp4" - only instances of Provider and Type are allowed, got: [?null?, ...]`); + })); + + it('should throw with descriptive error message when null or undefined is passed to module bootstrap', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({bootstrap: [null]}) + class ModuleWithNullBootstrap { + } + @NgModule({bootstrap: [undefined]}) + class ModuleWithUndefinedBootstrap { + } + + expect(() => resolver.loadNgModuleMetadata(ModuleWithNullBootstrap, true)) + .toThrowError( + `Unexpected value 'null' used in the bootstrap property of module 'ModuleWithNullBootstrap'`); + expect(() => resolver.loadNgModuleMetadata(ModuleWithUndefinedBootstrap, true)) + .toThrowError( + `Unexpected value 'undefined' used in the bootstrap property of module 'ModuleWithUndefinedBootstrap'`); + })); + + it('should throw an error when the interpolation config has invalid symbols', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + @NgModule({declarations: [ComponentWithInvalidInterpolation1]}) + class Module1 { + } + + expect(() => resolver.loadNgModuleMetadata(Module1, true)) + .toThrowError(`[' ', ' '] contains unusable interpolation symbol.`); + + @NgModule({declarations: [ComponentWithInvalidInterpolation2]}) + class Module2 { + } + + expect(() => resolver.loadNgModuleMetadata(Module2, true)) + .toThrowError(`['{', '}'] contains unusable interpolation symbol.`); + + @NgModule({declarations: [ComponentWithInvalidInterpolation3]}) + class Module3 { + } + + expect(() => resolver.loadNgModuleMetadata(Module3, true)) + .toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`); + + @NgModule({declarations: [ComponentWithInvalidInterpolation4]}) + class Module4 { + } + + expect(() => resolver.loadNgModuleMetadata(Module4, true)) + .toThrowError(`['&#', '}}'] contains unusable interpolation symbol.`); + + @NgModule({declarations: [ComponentWithInvalidInterpolation5]}) + class Module5 { + } + + expect(() => resolver.loadNgModuleMetadata(Module5, true)) + .toThrowError(`['{', '}}'] contains unusable interpolation symbol.`); + })); }); + + it('should dedupe declarations in NgModule', + inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => { + + @Component({template: ''}) + class MyComp { + } + + @NgModule({declarations: [MyComp, MyComp]}) + class MyModule { + } + + const modMeta = resolver.loadNgModuleMetadata(MyModule, true).ngModule; + expect(modMeta.declaredDirectives.length).toBe(1); + expect(modMeta.declaredDirectives[0].reference).toBe(MyComp); + })); } @Component({selector: 'someComponent', template: ''}) @@ -210,6 +331,14 @@ class ComponentWithoutModuleId { class ComponentWithInvalidModuleId { } +@Component({ + selector: 'someSelector', + templateUrl: 'someTemplateUrl', + styleUrls: ['someStyleUrl'], +}) +class ComponentWithExternalResources { +} + @Component({ selector: 'someSelector', inputs: ['someProp'], @@ -223,13 +352,11 @@ class ComponentWithInvalidModuleId { moduleId: 'someModuleId', changeDetection: ChangeDetectionStrategy.Default, template: 'someTemplate', - templateUrl: 'someTemplateUrl', encapsulation: ViewEncapsulation.Emulated, styles: ['someStyle'], - styleUrls: ['someStyleUrl'], interpolation: ['{{', '}}'] }) -class ComponentWithEverything implements OnChanges, +class ComponentWithEverythingInline implements OnChanges, OnInit, DoCheck, OnDestroy, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked { ngOnChanges(changes: SimpleChanges): void {} diff --git a/modules/@angular/compiler/test/runtime_compiler_spec.ts b/modules/@angular/compiler/test/runtime_compiler_spec.ts index 4c8d4f623d..b0e3c6dac0 100644 --- a/modules/@angular/compiler/test/runtime_compiler_spec.ts +++ b/modules/@angular/compiler/test/runtime_compiler_spec.ts @@ -8,8 +8,7 @@ import {DirectiveResolver, ResourceLoader} from '@angular/compiler'; import {Compiler, Component, Injector, NgModule, NgModuleFactory} from '@angular/core'; -import {TestBed, async, fakeAsync, tick} from '@angular/core/testing'; -import {beforeEach, describe, inject, it} from '@angular/core/testing/testing_internal'; +import {TestBed, async, fakeAsync, inject, tick} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/matchers'; import {stringify} from '../src/facade/lang'; diff --git a/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts b/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts index aa4e90aa7b..ba6bffc4af 100644 --- a/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts +++ b/modules/@angular/platform-browser/test/browser/bootstrap_spec.ts @@ -11,7 +11,7 @@ import {ApplicationRef, destroyPlatform} from '@angular/core/src/application_ref import {Console} from '@angular/core/src/console'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability'; -import {AsyncTestCompleter, Log, afterEach, beforeEach, beforeEachProviders, describe, inject, it} from '@angular/core/testing/testing_internal'; +import {AsyncTestCompleter, Log, afterEach, beforeEach, beforeEachProviders, describe, iit, inject, it} from '@angular/core/testing/testing_internal'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -149,11 +149,19 @@ export function main() { afterEach(destroyPlatform); - it('should throw if bootstrapped Directive is not a Component', () => { - expect(() => bootstrap(HelloRootDirectiveIsNotCmp)) - .toThrowError( - `Could not compile '${stringify(HelloRootDirectiveIsNotCmp)}' because it is not a component.`); - }); + it('should throw if bootstrapped Directive is not a Component', + inject([AsyncTestCompleter], (done: AsyncTestCompleter) => { + var logger = new MockConsole(); + var errorHandler = new ErrorHandler(false); + errorHandler._console = logger as any; + bootstrap(HelloRootDirectiveIsNotCmp, [ + {provide: ErrorHandler, useValue: errorHandler} + ]).catch((e) => { + expect(e.message).toBe( + `Could not compile '${stringify(HelloRootDirectiveIsNotCmp)}' because it is not a component.`); + done.done(); + }); + })); it('should throw if no element is found', inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {