diff --git a/modules/@angular/compiler-cli/src/codegen.ts b/modules/@angular/compiler-cli/src/codegen.ts index 40f7afa614..85e2ab5901 100644 --- a/modules/@angular/compiler-cli/src/codegen.ts +++ b/modules/@angular/compiler-cli/src/codegen.ts @@ -101,44 +101,23 @@ export class CodeGenerator { return path.join(this.options.genDir, path.relative(root, filePath)); } - // TODO(tbosch): add a cache for shared css files - // TODO(tbosch): detect cycles! - private generateStylesheet(filepath: string, shim: boolean): Promise { - return this.compiler.loadAndCompileStylesheet(filepath, shim, '.ts') - .then((sourceWithImports) => { - const emitPath = this.calculateEmitPath(sourceWithImports.source.moduleUrl); - // TODO(alexeagle): should include the sourceFile to the WriteFileCallback - this.host.writeFile(emitPath, PREAMBLE + sourceWithImports.source.source, false); - return Promise.all( - sourceWithImports.importedUrls.map(url => this.generateStylesheet(url, shim))); - }); - } - codegen(): Promise { - let stylesheetPromises: Promise[] = []; const generateOneFile = (absSourcePath: string) => Promise.all(this.readComponents(absSourcePath)) .then((metadatas: compiler.CompileDirectiveMetadata[]) => { if (!metadatas || !metadatas.length) { return; } - metadatas.forEach((metadata) => { - let stylesheetPaths = metadata && metadata.template && metadata.template.styleUrls; - if (stylesheetPaths) { - stylesheetPaths.forEach((path) => { - stylesheetPromises.push(this.generateStylesheet( - path, metadata.template.encapsulation === ViewEncapsulation.Emulated)); - }); - } - }); return this.generateSource(metadatas); }) - .then(generated => { - if (generated) { - const sourceFile = this.program.getSourceFile(absSourcePath); - const emitPath = this.calculateEmitPath(generated.moduleUrl); - this.host.writeFile( - emitPath, PREAMBLE + generated.source, false, () => {}, [sourceFile]); + .then(generatedModules => { + if (generatedModules) { + generatedModules.forEach((generatedModule) => { + const sourceFile = this.program.getSourceFile(absSourcePath); + const emitPath = this.calculateEmitPath(generatedModule.moduleUrl); + this.host.writeFile( + emitPath, PREAMBLE + generatedModule.source, false, () => {}, [sourceFile]); + }); } }) .catch((e) => { console.error(e.stack); }); @@ -146,7 +125,7 @@ export class CodeGenerator { .map(sf => sf.fileName) .filter(f => !GENERATED_FILES.test(f)) .map(generateOneFile); - return Promise.all(stylesheetPromises.concat(compPromises)); + return Promise.all(compPromises); } static create( @@ -173,7 +152,7 @@ export class CodeGenerator { /*console*/ null, []); const offlineCompiler = new compiler.OfflineCompiler( normalizer, tmplParser, new StyleCompiler(urlResolver), new ViewCompiler(config), - new TypeScriptEmitter(reflectorHost), xhr); + new TypeScriptEmitter(reflectorHost)); const resolver = new CompileMetadataResolver( new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), new compiler.ViewResolver(staticReflector), config, staticReflector); diff --git a/modules/@angular/compiler-cli/src/extract_i18n.ts b/modules/@angular/compiler-cli/src/extract_i18n.ts index 996e150a92..6c8dbec307 100644 --- a/modules/@angular/compiler-cli/src/extract_i18n.ts +++ b/modules/@angular/compiler-cli/src/extract_i18n.ts @@ -162,7 +162,7 @@ class Extractor { /*console*/ null, []); const offlineCompiler = new compiler.OfflineCompiler( normalizer, tmplParser, new StyleCompiler(urlResolver), new ViewCompiler(config), - new TypeScriptEmitter(reflectorHost), xhr); + new TypeScriptEmitter(reflectorHost)); const resolver = new CompileMetadataResolver( new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector), new compiler.ViewResolver(staticReflector), config, staticReflector); diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index 3e3cb5fb0f..93b2a3ed5d 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -600,6 +600,31 @@ export class CompileQueryMetadata { } } +/** + * Metadata about a stylesheet + */ +export class CompileStylesheetMetadata { + moduleUrl: string; + styles: string[]; + styleUrls: string[]; + constructor( + {moduleUrl, styles, + styleUrls}: {moduleUrl?: string, styles?: string[], styleUrls?: string[]} = {}) { + this.moduleUrl = moduleUrl; + this.styles = _normalizeArray(styles); + this.styleUrls = _normalizeArray(styleUrls); + } + + static fromJson(data: {[key: string]: any}): CompileStylesheetMetadata { + return new CompileStylesheetMetadata( + {moduleUrl: data['moduleUrl'], styles: data['styles'], styleUrls: data['styleUrls']}); + } + + toJson(): {[key: string]: any} { + return {'moduleUrl': this.moduleUrl, 'styles': this.styles, 'styleUrls': this.styleUrls}; + } +} + /** * Metadata regarding compilation of a template. */ @@ -609,17 +634,19 @@ export class CompileTemplateMetadata { templateUrl: string; styles: string[]; styleUrls: string[]; + externalStylesheets: CompileStylesheetMetadata[]; animations: CompileAnimationEntryMetadata[]; ngContentSelectors: string[]; interpolation: [string, string]; constructor( - {encapsulation, template, templateUrl, styles, styleUrls, animations, ngContentSelectors, - interpolation}: { + {encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations, + ngContentSelectors, interpolation}: { encapsulation?: ViewEncapsulation, template?: string, templateUrl?: string, styles?: string[], styleUrls?: string[], + externalStylesheets?: CompileStylesheetMetadata[], ngContentSelectors?: string[], animations?: CompileAnimationEntryMetadata[], interpolation?: [string, string] @@ -627,8 +654,9 @@ export class CompileTemplateMetadata { this.encapsulation = encapsulation; this.template = template; this.templateUrl = templateUrl; - this.styles = isPresent(styles) ? styles : []; - this.styleUrls = isPresent(styleUrls) ? styleUrls : []; + this.styles = _normalizeArray(styles); + this.styleUrls = _normalizeArray(styleUrls); + this.externalStylesheets = _normalizeArray(externalStylesheets); this.animations = isPresent(animations) ? ListWrapper.flatten(animations) : []; this.ngContentSelectors = isPresent(ngContentSelectors) ? ngContentSelectors : []; if (isPresent(interpolation) && interpolation.length != 2) { @@ -648,6 +676,8 @@ export class CompileTemplateMetadata { templateUrl: data['templateUrl'], styles: data['styles'], styleUrls: data['styleUrls'], + externalStylesheets: + _arrayFromJson(data['externalStylesheets'], CompileStylesheetMetadata.fromJson), animations: animations, ngContentSelectors: data['ngContentSelectors'], interpolation: data['interpolation'] @@ -662,6 +692,7 @@ export class CompileTemplateMetadata { 'templateUrl': this.templateUrl, 'styles': this.styles, 'styleUrls': this.styleUrls, + 'externalStylesheets': _objToJson(this.externalStylesheets), 'animations': _objToJson(this.animations), 'ngContentSelectors': this.ngContentSelectors, 'interpolation': this.interpolation diff --git a/modules/@angular/compiler/src/compiler.ts b/modules/@angular/compiler/src/compiler.ts index 29372ced4c..ee0867b33f 100644 --- a/modules/@angular/compiler/src/compiler.ts +++ b/modules/@angular/compiler/src/compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentResolver, Type} from '@angular/core'; +import {Compiler, ComponentResolver, Type} from '@angular/core'; export * from './template_ast'; export {TEMPLATE_TRANSFORMS} from './template_parser'; @@ -49,6 +49,7 @@ export const COMPILER_PROVIDERS: Array = /*@ts2dart_Provider*/ {provide: CompilerConfig, useValue: new CompilerConfig()}, RuntimeCompiler, /*@ts2dart_Provider*/ {provide: ComponentResolver, useExisting: RuntimeCompiler}, + /*@ts2dart_Provider*/ {provide: Compiler, useExisting: RuntimeCompiler}, DomElementSchemaRegistry, /*@ts2dart_Provider*/ {provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry}, UrlResolver, ViewResolver, DirectiveResolver, PipeResolver diff --git a/modules/@angular/compiler/src/directive_normalizer.ts b/modules/@angular/compiler/src/directive_normalizer.ts index 83d812f47e..75dee4d97f 100644 --- a/modules/@angular/compiler/src/directive_normalizer.ts +++ b/modules/@angular/compiler/src/directive_normalizer.ts @@ -12,65 +12,92 @@ import {PromiseWrapper} from '../src/facade/async'; import {BaseException} from '../src/facade/exceptions'; import {isBlank, isPresent} from '../src/facade/lang'; -import {CompileTypeMetadata, CompileDirectiveMetadata, CompileTemplateMetadata,} from './compile_metadata'; -import {XHR} from './xhr'; -import {UrlResolver} from './url_resolver'; -import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver'; - -import {HtmlAstVisitor, HtmlElementAst, HtmlTextAst, HtmlAttrAst, HtmlCommentAst, HtmlExpansionAst, HtmlExpansionCaseAst, htmlVisitAll} from './html_ast'; -import {HtmlParser} from './html_parser'; +import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from './compile_metadata'; import {CompilerConfig} from './config'; +import {HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from './html_ast'; +import {HtmlParser} from './html_parser'; +import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver'; +import {PreparsedElementType, preparseElement} from './template_preparser'; +import {UrlResolver} from './url_resolver'; +import {XHR} from './xhr'; -import {preparseElement, PreparsedElementType} from './template_preparser'; - +export class NormalizeDirectiveResult { + constructor( + public syncResult: CompileDirectiveMetadata, + public asyncResult: Promise) {} +} @Injectable() export class DirectiveNormalizer { + private _xhrCache = new Map>(); + constructor( private _xhr: XHR, private _urlResolver: UrlResolver, private _htmlParser: HtmlParser, private _config: CompilerConfig) {} - normalizeDirective(directive: CompileDirectiveMetadata): Promise { - if (!directive.isComponent) { - // For non components there is nothing to be normalized yet. - return PromiseWrapper.resolve(directive); + clearCache() { this._xhrCache.clear(); } + + clearCacheFor(normalizedDirective: CompileDirectiveMetadata) { + if (!normalizedDirective.isComponent) { + return; } - return this.normalizeTemplate(directive.type, directive.template) - .then((normalizedTemplate: CompileTemplateMetadata) => 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, - lifecycleHooks: directive.lifecycleHooks, - providers: directive.providers, - viewProviders: directive.viewProviders, - queries: directive.queries, - viewQueries: directive.viewQueries, - precompile: directive.precompile, - template: normalizedTemplate - })); + this._xhrCache.delete(normalizedDirective.template.templateUrl); + normalizedDirective.template.externalStylesheets.forEach( + (stylesheet) => { this._xhrCache.delete(stylesheet.moduleUrl); }); } - normalizeTemplate(directiveType: CompileTypeMetadata, template: CompileTemplateMetadata): - Promise { - if (isPresent(template.template)) { - return PromiseWrapper.resolve(this.normalizeLoadedTemplate( - directiveType, template, template.template, directiveType.moduleUrl)); - } else if (isPresent(template.templateUrl)) { - var sourceAbsUrl = this._urlResolver.resolve(directiveType.moduleUrl, template.templateUrl); - return this._xhr.get(sourceAbsUrl) - .then( - templateContent => this.normalizeLoadedTemplate( - directiveType, template, templateContent, sourceAbsUrl)); - } else { - throw new BaseException(`No template specified for component ${directiveType.name}`); + private _fetch(url: string): Promise { + var result = this._xhrCache.get(url); + if (!result) { + result = this._xhr.get(url); + this._xhrCache.set(url, result); } + return result; + } + + normalizeDirective(directive: CompileDirectiveMetadata): NormalizeDirectiveResult { + if (!directive.isComponent) { + // For non components there is nothing to be normalized yet. + return new NormalizeDirectiveResult(directive, Promise.resolve(directive)); + } + let normalizedTemplateSync: CompileTemplateMetadata = null; + let normalizedTemplateAsync: Promise; + if (isPresent(directive.template.template)) { + normalizedTemplateSync = this.normalizeTemplateSync(directive.type, directive.template); + normalizedTemplateAsync = Promise.resolve(normalizedTemplateSync); + } else if (directive.template.templateUrl) { + normalizedTemplateAsync = this.normalizeTemplateAsync(directive.type, directive.template); + } else { + throw new BaseException(`No template specified for component ${directive.type.name}`); + } + if (normalizedTemplateSync && normalizedTemplateSync.styleUrls.length === 0) { + // sync case + let normalizedDirective = _cloneDirectiveWithTemplate(directive, normalizedTemplateSync); + return new NormalizeDirectiveResult( + normalizedDirective, Promise.resolve(normalizedDirective)); + } else { + // async case + return new NormalizeDirectiveResult( + null, + normalizedTemplateAsync + .then((normalizedTemplate) => this.normalizeExternalStylesheets(normalizedTemplate)) + .then( + (normalizedTemplate) => + _cloneDirectiveWithTemplate(directive, normalizedTemplate))); + } + } + + normalizeTemplateSync(directiveType: CompileTypeMetadata, template: CompileTemplateMetadata): + CompileTemplateMetadata { + return this.normalizeLoadedTemplate( + directiveType, template, template.template, directiveType.moduleUrl); + } + + normalizeTemplateAsync(directiveType: CompileTypeMetadata, template: CompileTemplateMetadata): + Promise { + let templateUrl = this._urlResolver.resolve(directiveType.moduleUrl, template.templateUrl); + return this._fetch(templateUrl) + .then((value) => this.normalizeLoadedTemplate(directiveType, template, value, templateUrl)); } normalizeLoadedTemplate( @@ -81,42 +108,87 @@ export class DirectiveNormalizer { var errorString = rootNodesAndErrors.errors.join('\n'); throw new BaseException(`Template parse errors:\n${errorString}`); } + var templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({ + styles: templateMeta.styles, + styleUrls: templateMeta.styleUrls, + moduleUrl: directiveType.moduleUrl + })); var visitor = new TemplatePreparseVisitor(); htmlVisitAll(visitor, rootNodesAndErrors.rootNodes); - var allStyles = templateMeta.styles.concat(visitor.styles); + var templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata( + {styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl})); - var allStyleAbsUrls = - visitor.styleUrls.filter(isStyleUrlResolvable) - .map(url => this._urlResolver.resolve(templateAbsUrl, url)) - .concat(templateMeta.styleUrls.filter(isStyleUrlResolvable) - .map(url => this._urlResolver.resolve(directiveType.moduleUrl, url))); - - var allResolvedStyles = allStyles.map(style => { - var styleWithImports = extractStyleUrls(this._urlResolver, templateAbsUrl, style); - styleWithImports.styleUrls.forEach(styleUrl => allStyleAbsUrls.push(styleUrl)); - return styleWithImports.style; - }); + var allStyles = templateMetadataStyles.styles.concat(templateStyles.styles); + var allStyleUrls = templateMetadataStyles.styleUrls.concat(templateStyles.styleUrls); var encapsulation = templateMeta.encapsulation; if (isBlank(encapsulation)) { encapsulation = this._config.defaultEncapsulation; } - if (encapsulation === ViewEncapsulation.Emulated && allResolvedStyles.length === 0 && - allStyleAbsUrls.length === 0) { + if (encapsulation === ViewEncapsulation.Emulated && allStyles.length === 0 && + allStyleUrls.length === 0) { encapsulation = ViewEncapsulation.None; } return new CompileTemplateMetadata({ encapsulation: encapsulation, template: template, templateUrl: templateAbsUrl, - styles: allResolvedStyles, - styleUrls: allStyleAbsUrls, + styles: allStyles, + styleUrls: allStyleUrls, + externalStylesheets: templateMeta.externalStylesheets, ngContentSelectors: visitor.ngContentSelectors, animations: templateMeta.animations, interpolation: templateMeta.interpolation }); } + + normalizeExternalStylesheets(templateMeta: CompileTemplateMetadata): + Promise { + return this._loadMissingExternalStylesheets(templateMeta.styleUrls) + .then((externalStylesheets) => new CompileTemplateMetadata({ + encapsulation: templateMeta.encapsulation, + template: templateMeta.template, + templateUrl: templateMeta.templateUrl, + styles: templateMeta.styles, + styleUrls: templateMeta.styleUrls, + externalStylesheets: externalStylesheets, + ngContentSelectors: templateMeta.ngContentSelectors, + animations: templateMeta.animations, + interpolation: templateMeta.interpolation + })); + } + + private _loadMissingExternalStylesheets( + styleUrls: string[], + loadedStylesheets: + Map = new Map()): + Promise { + return Promise + .all(styleUrls.filter((styleUrl) => !loadedStylesheets.has(styleUrl)) + .map(styleUrl => this._fetch(styleUrl).then((loadedStyle) => { + var stylesheet = this.normalizeStylesheet( + new CompileStylesheetMetadata({styles: [loadedStyle], moduleUrl: styleUrl})); + loadedStylesheets.set(styleUrl, stylesheet); + return this._loadMissingExternalStylesheets( + stylesheet.styleUrls, loadedStylesheets); + }))) + .then((_) => Array.from(loadedStylesheets.values())); + } + + normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata { + var allStyleUrls = stylesheet.styleUrls.filter(isStyleUrlResolvable) + .map(url => this._urlResolver.resolve(stylesheet.moduleUrl, url)); + + var allStyles = stylesheet.styles.map(style => { + var styleWithImports = extractStyleUrls(this._urlResolver, stylesheet.moduleUrl, style); + allStyleUrls.push(...styleWithImports.styleUrls); + return styleWithImports.style; + }); + + return new CompileStylesheetMetadata( + {styles: allStyles, styleUrls: allStyleUrls, moduleUrl: stylesheet.moduleUrl}); + } } class TemplatePreparseVisitor implements HtmlAstVisitor { @@ -166,3 +238,27 @@ class TemplatePreparseVisitor implements HtmlAstVisitor { visitExpansionCase(ast: HtmlExpansionCaseAst, 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, + lifecycleHooks: directive.lifecycleHooks, + providers: directive.providers, + viewProviders: directive.viewProviders, + queries: directive.queries, + viewQueries: directive.viewQueries, + precompile: directive.precompile, + template: template + }); +} diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index 51040e844a..08c6d4e480 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -49,6 +49,16 @@ export class CompileMetadataResolver { return sanitizeIdentifier(identifier); } + clearCacheFor(compType: Type) { + this._directiveCache.delete(compType); + this._pipeCache.delete(compType); + } + + clearCache() { + this._directiveCache.clear(); + this._pipeCache.clear(); + } + getAnimationEntryMetadata(entry: AnimationEntryMetadata): cpl.CompileAnimationEntryMetadata { var defs = entry.definitions.map(def => this.getAnimationStateMetadata(def)); return new cpl.CompileAnimationEntryMetadata(entry.name, defs); @@ -101,7 +111,6 @@ export class CompileMetadataResolver { var moduleUrl = staticTypeModuleUrl(directiveType); var precompileTypes: cpl.CompileTypeMetadata[] = []; if (dirMeta instanceof ComponentMetadata) { - assertArrayOfStrings('styles', dirMeta.styles); var cmpMeta = dirMeta; var viewMeta = this._viewResolver.resolve(directiveType); assertArrayOfStrings('styles', viewMeta.styles); @@ -109,6 +118,8 @@ export class CompileMetadataResolver { var animations = isPresent(viewMeta.animations) ? viewMeta.animations.map(e => this.getAnimationEntryMetadata(e)) : null; + assertArrayOfStrings('styles', viewMeta.styles); + assertArrayOfStrings('styleUrls', viewMeta.styleUrls); templateMeta = new cpl.CompileTemplateMetadata({ encapsulation: viewMeta.encapsulation, diff --git a/modules/@angular/compiler/src/offline_compiler.ts b/modules/@angular/compiler/src/offline_compiler.ts index c30e59f8eb..1d6b2ae774 100644 --- a/modules/@angular/compiler/src/offline_compiler.ts +++ b/modules/@angular/compiler/src/offline_compiler.ts @@ -14,7 +14,7 @@ import {ListWrapper} from './facade/collection'; import {BaseException} from './facade/exceptions'; import {OutputEmitter} from './output/abstract_emitter'; import * as o from './output/output_ast'; -import {StyleCompiler, StylesCompileResult} from './style_compiler'; +import {CompiledStylesheet, StyleCompiler} from './style_compiler'; import {TemplateParser} from './template_parser'; import {assetUrl} from './util'; import {ComponentFactoryDependency, ViewCompileResult, ViewCompiler, ViewFactoryDependency} from './view_compiler/view_compiler'; @@ -44,29 +44,38 @@ export class OfflineCompiler { constructor( private _directiveNormalizer: DirectiveNormalizer, private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, - private _outputEmitter: OutputEmitter, private _xhr: XHR) {} + private _outputEmitter: OutputEmitter) {} normalizeDirectiveMetadata(directive: CompileDirectiveMetadata): Promise { - return this._directiveNormalizer.normalizeDirective(directive); + return this._directiveNormalizer.normalizeDirective(directive).asyncResult; } - compileTemplates(components: NormalizedComponentWithViewDirectives[]): SourceModule { + compileTemplates(components: NormalizedComponentWithViewDirectives[]): SourceModule[] { if (components.length === 0) { throw new BaseException('No components given'); } var statements: o.DeclareVarStmt[] = []; var exportedVars: string[] = []; var moduleUrl = _ngfactoryModuleUrl(components[0].component.type); + var outputSourceModules: SourceModule[] = []; components.forEach(componentWithDirs => { var compMeta = componentWithDirs.component; _assertComponent(compMeta); + var fileSuffix = _splitLastSuffix(compMeta.type.moduleUrl)[1]; + var stylesCompileResults = this._styleCompiler.compileComponent(compMeta); + stylesCompileResults.externalStylesheets.forEach((compiledStyleSheet) => { + outputSourceModules.push(this._codgenStyles(compiledStyleSheet, fileSuffix)); + }); + var compViewFactoryVar = this._compileComponent( - compMeta, componentWithDirs.directives, componentWithDirs.pipes, statements); + compMeta, componentWithDirs.directives, componentWithDirs.pipes, + stylesCompileResults.componentStylesheet, fileSuffix, statements); exportedVars.push(compViewFactoryVar); var hostMeta = createHostComponentMeta(compMeta.type, compMeta.selector); - var hostViewFactoryVar = this._compileComponent(hostMeta, [compMeta], [], statements); + var hostViewFactoryVar = + this._compileComponent(hostMeta, [compMeta], [], null, fileSuffix, statements); var compFactoryVar = _componentFactoryName(compMeta.type); statements.push( o.variable(compFactoryVar) @@ -82,43 +91,32 @@ export class OfflineCompiler { .toDeclStmt(null, [o.StmtModifier.Final])); exportedVars.push(compFactoryVar); }); - return this._codegenSourceModule(moduleUrl, statements, exportedVars); - } - - loadAndCompileStylesheet(stylesheetUrl: string, shim: boolean, suffix: string): - Promise { - return this._xhr.get(stylesheetUrl).then((cssText) => { - var compileResult = this._styleCompiler.compileStylesheet(stylesheetUrl, cssText, shim); - var importedUrls: string[] = []; - compileResult.dependencies.forEach((dep) => { - importedUrls.push(dep.moduleUrl); - dep.valuePlaceholder.moduleUrl = _stylesModuleUrl(dep.moduleUrl, dep.isShimmed, suffix); - }); - return new StyleSheetSourceWithImports( - this._codgenStyles(stylesheetUrl, shim, suffix, compileResult), importedUrls); - }); + outputSourceModules.unshift(this._codegenSourceModule(moduleUrl, statements, exportedVars)); + return outputSourceModules; } private _compileComponent( compMeta: CompileDirectiveMetadata, directives: CompileDirectiveMetadata[], - pipes: CompilePipeMetadata[], targetStatements: o.Statement[]): string { - var styleResult = this._styleCompiler.compileComponent(compMeta); + pipes: CompilePipeMetadata[], componentStyles: CompiledStylesheet, fileSuffix: string, + targetStatements: o.Statement[]): string { var parsedTemplate = this._templateParser.parse( compMeta, compMeta.template.template, directives, pipes, compMeta.type.name); - var viewResult = this._viewCompiler.compileComponent( - compMeta, parsedTemplate, o.variable(styleResult.stylesVar), pipes); - ListWrapper.addAll( - targetStatements, _resolveStyleStatements(compMeta.type.moduleUrl, styleResult)); + var stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); + var viewResult = + this._viewCompiler.compileComponent(compMeta, parsedTemplate, stylesExpr, pipes); + if (componentStyles) { + ListWrapper.addAll(targetStatements, _resolveStyleStatements(componentStyles, fileSuffix)); + } ListWrapper.addAll(targetStatements, _resolveViewStatements(viewResult)); return viewResult.viewFactoryVar; } - private _codgenStyles( - inputUrl: string, shim: boolean, suffix: string, - stylesCompileResult: StylesCompileResult): SourceModule { + private _codgenStyles(stylesCompileResult: CompiledStylesheet, fileSuffix: string): SourceModule { + _resolveStyleStatements(stylesCompileResult, fileSuffix); return this._codegenSourceModule( - _stylesModuleUrl(inputUrl, shim, suffix), stylesCompileResult.statements, - [stylesCompileResult.stylesVar]); + _stylesModuleUrl( + stylesCompileResult.meta.moduleUrl, stylesCompileResult.isShimmed, fileSuffix), + stylesCompileResult.statements, [stylesCompileResult.stylesVar]); } private _codegenSourceModule( @@ -131,7 +129,7 @@ export class OfflineCompiler { function _resolveViewStatements(compileResult: ViewCompileResult): o.Statement[] { compileResult.dependencies.forEach((dep) => { if (dep instanceof ViewFactoryDependency) { - dep.placeholder.moduleUrl = _ngfactoryModuleUrl(dep.comp.type); + dep.placeholder.moduleUrl = _ngfactoryModuleUrl(dep.comp); } else if (dep instanceof ComponentFactoryDependency) { dep.placeholder.name = _componentFactoryName(dep.comp); dep.placeholder.moduleUrl = _ngfactoryModuleUrl(dep.comp); @@ -142,17 +140,15 @@ function _resolveViewStatements(compileResult: ViewCompileResult): o.Statement[] function _resolveStyleStatements( - containingModuleUrl: string, compileResult: StylesCompileResult): o.Statement[] { - var containingSuffix = _splitSuffix(containingModuleUrl)[1]; + compileResult: CompiledStylesheet, fileSuffix: string): o.Statement[] { compileResult.dependencies.forEach((dep) => { - dep.valuePlaceholder.moduleUrl = - _stylesModuleUrl(dep.moduleUrl, dep.isShimmed, containingSuffix); + dep.valuePlaceholder.moduleUrl = _stylesModuleUrl(dep.moduleUrl, dep.isShimmed, fileSuffix); }); return compileResult.statements; } function _ngfactoryModuleUrl(comp: CompileIdentifierMetadata): string { - var urlWithSuffix = _splitSuffix(comp.moduleUrl); + var urlWithSuffix = _splitLastSuffix(comp.moduleUrl); return `${urlWithSuffix[0]}.ngfactory${urlWithSuffix[1]}`; } @@ -170,7 +166,7 @@ function _assertComponent(meta: CompileDirectiveMetadata) { } } -function _splitSuffix(path: string): string[] { +function _splitLastSuffix(path: string): string[] { let lastDot = path.lastIndexOf('.'); if (lastDot !== -1) { return [path.substring(0, lastDot), path.substring(lastDot)]; diff --git a/modules/@angular/compiler/src/runtime_compiler.ts b/modules/@angular/compiler/src/runtime_compiler.ts index 7c1d280083..f532e008fe 100644 --- a/modules/@angular/compiler/src/runtime_compiler.ts +++ b/modules/@angular/compiler/src/runtime_compiler.ts @@ -6,26 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactory, ComponentResolver, Injectable} from '@angular/core'; +import {Compiler, ComponentFactory, ComponentResolver, Injectable} from '@angular/core'; import {BaseException} from '../src/facade/exceptions'; -import {IS_DART, Type, isBlank, isString} from '../src/facade/lang'; +import {ConcreteType, IS_DART, Type, isBlank, isString, stringify} from '../src/facade/lang'; import {ListWrapper,} from '../src/facade/collection'; import {PromiseWrapper} from '../src/facade/async'; import {createHostComponentMeta, CompileDirectiveMetadata, CompilePipeMetadata, CompileIdentifierMetadata} from './compile_metadata'; import {TemplateAst,} from './template_ast'; -import {StyleCompiler, StylesCompileDependency, StylesCompileResult} from './style_compiler'; -import {ViewCompiler, ViewFactoryDependency, ComponentFactoryDependency} from './view_compiler/view_compiler'; +import {StyleCompiler, StylesCompileDependency, CompiledStylesheet} from './style_compiler'; +import {ViewCompiler, ViewCompileResult, ViewFactoryDependency, ComponentFactoryDependency} from './view_compiler/view_compiler'; import {TemplateParser} from './template_parser'; -import {DirectiveNormalizer} from './directive_normalizer'; +import {DirectiveNormalizer, NormalizeDirectiveResult} from './directive_normalizer'; import {CompileMetadataResolver} from './metadata_resolver'; import {CompilerConfig} from './config'; import * as ir from './output/output_ast'; import {jitStatements} from './output/output_jit'; import {interpretStatements} from './output/output_interpreter'; import {InterpretiveAppViewInstanceFactory} from './output/interpretive_view'; -import {XHR} from './xhr'; /** * An internal module of the Angular compiler that begins with component types, @@ -33,15 +32,14 @@ import {XHR} from './xhr'; * ready for linking into an application. */ @Injectable() -export class RuntimeCompiler implements ComponentResolver { - private _styleCache: Map> = new Map>(); - private _hostCacheKeys = new Map(); +export class RuntimeCompiler implements ComponentResolver, Compiler { private _compiledTemplateCache = new Map(); + private _compiledHostTemplateCache = new Map(); constructor( private _metadataResolver: CompileMetadataResolver, private _templateNormalizer: DirectiveNormalizer, private _templateParser: TemplateParser, - private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _xhr: XHR, + private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _genConfig: CompilerConfig) {} resolveComponent(component: Type|string): Promise> { @@ -49,175 +47,215 @@ export class RuntimeCompiler implements ComponentResolver { return PromiseWrapper.reject( new BaseException(`Cannot resolve component using '${component}'.`), null); } - return this._loadAndCompileHostComponent(component).done; + return this.compileComponentAsync(>component); + } + + compileComponentAsync(compType: ConcreteType): Promise> { + var templates = this._getTransitiveCompiledTemplates(compType, true); + var loadingPromises: Promise[] = []; + templates.forEach((template) => { + if (template.loading) { + loadingPromises.push(template.loading); + } + }); + return Promise.all(loadingPromises).then(() => { + templates.forEach((template) => { this._compileTemplate(template); }); + return this._getCompiledHostTemplate(compType).proxyComponentFactory; + }); + } + + compileComponentSync(compType: ConcreteType): ComponentFactory { + var templates = this._getTransitiveCompiledTemplates(compType, true); + templates.forEach((template) => { + if (template.loading) { + throw new BaseException( + `Can't compile synchronously as ${template.compType.name} is still being loaded!`); + } + }); + templates.forEach((template) => { this._compileTemplate(template); }); + return this._getCompiledHostTemplate(compType).proxyComponentFactory; + } + + clearCacheFor(compType: Type) { + this._metadataResolver.clearCacheFor(compType); + this._compiledHostTemplateCache.delete(compType); + var compiledTemplate = this._compiledTemplateCache.get(compType); + if (compiledTemplate) { + this._templateNormalizer.clearCacheFor(compiledTemplate.normalizedCompMeta); + this._compiledTemplateCache.delete(compType); + } } clearCache(): void { - this._styleCache.clear(); + this._metadataResolver.clearCache(); this._compiledTemplateCache.clear(); - this._hostCacheKeys.clear(); + this._compiledHostTemplateCache.clear(); + this._templateNormalizer.clearCache(); } - private _loadAndCompileHostComponent(componentType: Type): CompileHostTemplate { - var compMeta: CompileDirectiveMetadata = - this._metadataResolver.getDirectiveMetadata(componentType); - var hostCacheKey = this._hostCacheKeys.get(compMeta.type.runtime); - if (isBlank(hostCacheKey)) { - hostCacheKey = new Object(); - this._hostCacheKeys.set(compMeta.type.runtime, hostCacheKey); - assertComponent(compMeta); - var hostMeta: CompileDirectiveMetadata = - createHostComponentMeta(compMeta.type, compMeta.selector); - - this._loadAndCompileComponent(hostCacheKey, hostMeta, [compMeta], [], []); - } - var compTemplate = this._compiledTemplateCache.get(hostCacheKey); - return new CompileHostTemplate(compTemplate, compMeta); - } - - private _loadAndCompileComponent( - cacheKey: any, compMeta: CompileDirectiveMetadata, viewDirectives: CompileDirectiveMetadata[], - pipes: CompilePipeMetadata[], compilingComponentsPath: any[]): CompiledTemplate { - var compiledTemplate = this._compiledTemplateCache.get(cacheKey); + private _getCompiledHostTemplate(type: Type): CompiledTemplate { + var compiledTemplate = this._compiledHostTemplateCache.get(type); if (isBlank(compiledTemplate)) { - let done = - PromiseWrapper - .all([this._compileComponentStyles(compMeta)].concat(viewDirectives.map( - dirMeta => this._templateNormalizer.normalizeDirective(dirMeta)))) - .then((stylesAndNormalizedViewDirMetas: any[]) => { - var normalizedViewDirMetas = stylesAndNormalizedViewDirMetas.slice(1); - var styles = stylesAndNormalizedViewDirMetas[0]; - var parsedTemplate = this._templateParser.parse( - compMeta, compMeta.template.template, normalizedViewDirMetas, pipes, - compMeta.type.name); - - var childPromises: Promise[] = []; - compiledTemplate.init(this._compileComponent( - compMeta, parsedTemplate, styles, pipes, compilingComponentsPath, - childPromises)); - return PromiseWrapper.all(childPromises).then((_) => { return compiledTemplate; }); - }); - compiledTemplate = new CompiledTemplate(done); - this._compiledTemplateCache.set(cacheKey, compiledTemplate); + var compMeta = this._metadataResolver.getDirectiveMetadata(type); + assertComponent(compMeta); + var hostMeta = createHostComponentMeta(compMeta.type, compMeta.selector); + compiledTemplate = new CompiledTemplate( + true, compMeta.selector, compMeta.type, [], [type], [], [], + this._templateNormalizer.normalizeDirective(hostMeta)); + this._compiledHostTemplateCache.set(type, compiledTemplate); } return compiledTemplate; } - private _compileComponent( - compMeta: CompileDirectiveMetadata, parsedTemplate: TemplateAst[], styles: string[], - pipes: CompilePipeMetadata[], compilingComponentsPath: any[], - childPromises: Promise[]): Function { - var compileResult = this._viewCompiler.compileComponent( - compMeta, parsedTemplate, - new ir.ExternalExpr(new CompileIdentifierMetadata({runtime: styles})), pipes); - compileResult.dependencies.forEach((dep) => { - if (dep instanceof ViewFactoryDependency) { - let childCompilingComponentsPath = ListWrapper.clone(compilingComponentsPath); - let childCacheKey = dep.comp.type.runtime; - let childViewDirectives: CompileDirectiveMetadata[] = - this._metadataResolver.getViewDirectivesMetadata(dep.comp.type.runtime); - let childViewPipes: CompilePipeMetadata[] = - this._metadataResolver.getViewPipesMetadata(dep.comp.type.runtime); - let childIsRecursive = childCompilingComponentsPath.indexOf(childCacheKey) > -1; - childCompilingComponentsPath.push(childCacheKey); - - let childComp = this._loadAndCompileComponent( - dep.comp.type.runtime, dep.comp, childViewDirectives, childViewPipes, - childCompilingComponentsPath); - dep.placeholder.runtime = childComp.proxyViewFactory; - dep.placeholder.name = `viewFactory_${dep.comp.type.name}`; - if (!childIsRecursive) { - // Only wait for a child if it is not a cycle - childPromises.push(childComp.done); + private _getCompiledTemplate(type: Type): CompiledTemplate { + var compiledTemplate = this._compiledTemplateCache.get(type); + if (isBlank(compiledTemplate)) { + var compMeta = this._metadataResolver.getDirectiveMetadata(type); + assertComponent(compMeta); + var viewDirectives: CompileDirectiveMetadata[] = []; + var viewComponentTypes: Type[] = []; + this._metadataResolver.getViewDirectivesMetadata(type).forEach(dirOrComp => { + if (dirOrComp.isComponent) { + viewComponentTypes.push(dirOrComp.type.runtime); + } else { + viewDirectives.push(dirOrComp); } + }); + var precompileComponentTypes = compMeta.precompile.map((typeMeta) => typeMeta.runtime); + var pipes = this._metadataResolver.getViewPipesMetadata(type); + compiledTemplate = new CompiledTemplate( + false, compMeta.selector, compMeta.type, viewDirectives, viewComponentTypes, + precompileComponentTypes, pipes, this._templateNormalizer.normalizeDirective(compMeta)); + this._compiledTemplateCache.set(type, compiledTemplate); + } + return compiledTemplate; + } + + private _getTransitiveCompiledTemplates( + compType: Type, isHost: boolean, + target: Set = new Set()): Set { + var template = + isHost ? this._getCompiledHostTemplate(compType) : this._getCompiledTemplate(compType); + if (!target.has(template)) { + target.add(template); + template.viewComponentTypes.forEach( + (compType) => { this._getTransitiveCompiledTemplates(compType, false, target); }); + template.precompileHostComponentTypes.forEach( + (compType) => { this._getTransitiveCompiledTemplates(compType, true, target); }); + } + return target; + } + + private _compileTemplate(template: CompiledTemplate) { + if (template.isCompiled) { + return; + } + var compMeta = template.normalizedCompMeta; + var externalStylesheetsByModuleUrl = new Map(); + var stylesCompileResult = this._styleCompiler.compileComponent(compMeta); + stylesCompileResult.externalStylesheets.forEach( + (r) => { externalStylesheetsByModuleUrl.set(r.meta.moduleUrl, r); }); + this._resolveStylesCompileResult( + stylesCompileResult.componentStylesheet, externalStylesheetsByModuleUrl); + var viewCompMetas = template.viewComponentTypes.map( + (compType) => this._getCompiledTemplate(compType).normalizedCompMeta); + var parsedTemplate = this._templateParser.parse( + compMeta, compMeta.template.template, template.viewDirectives.concat(viewCompMetas), + template.viewPipes, compMeta.type.name); + var compileResult = this._viewCompiler.compileComponent( + compMeta, parsedTemplate, ir.variable(stylesCompileResult.componentStylesheet.stylesVar), + template.viewPipes); + var depTemplates = compileResult.dependencies.map((dep) => { + let depTemplate: CompiledTemplate; + if (dep instanceof ViewFactoryDependency) { + depTemplate = this._getCompiledTemplate(dep.comp.runtime); + dep.placeholder.runtime = depTemplate.proxyViewFactory; + dep.placeholder.name = `viewFactory_${dep.comp.name}`; } else if (dep instanceof ComponentFactoryDependency) { - let childComp = this._loadAndCompileHostComponent(dep.comp.runtime); - dep.placeholder.runtime = childComp.componentFactory; + depTemplate = this._getCompiledHostTemplate(dep.comp.runtime); + dep.placeholder.runtime = depTemplate.proxyComponentFactory; dep.placeholder.name = `compFactory_${dep.comp.name}`; - childPromises.push(childComp.done); } + return depTemplate; }); + var statements = + stylesCompileResult.componentStylesheet.statements.concat(compileResult.statements); var factory: any; if (IS_DART || !this._genConfig.useJit) { factory = interpretStatements( - compileResult.statements, compileResult.viewFactoryVar, - new InterpretiveAppViewInstanceFactory()); + statements, compileResult.viewFactoryVar, new InterpretiveAppViewInstanceFactory()); } else { factory = jitStatements( - `${compMeta.type.name}.template.js`, compileResult.statements, - compileResult.viewFactoryVar); + `${template.compType.name}.template.js`, statements, compileResult.viewFactoryVar); } - return factory; + template.compiled(factory); } - private _compileComponentStyles(compMeta: CompileDirectiveMetadata): Promise { - var compileResult = this._styleCompiler.compileComponent(compMeta); - return this._resolveStylesCompileResult(compMeta.type.name, compileResult); + private _resolveStylesCompileResult( + result: CompiledStylesheet, externalStylesheetsByModuleUrl: Map) { + result.dependencies.forEach((dep, i) => { + var nestedCompileResult = externalStylesheetsByModuleUrl.get(dep.moduleUrl); + var nestedStylesArr = this._resolveAndEvalStylesCompileResult( + nestedCompileResult, externalStylesheetsByModuleUrl); + dep.valuePlaceholder.runtime = nestedStylesArr; + dep.valuePlaceholder.name = `importedStyles${i}`; + }); } - private _resolveStylesCompileResult(sourceUrl: string, result: StylesCompileResult): - Promise { - var promises = result.dependencies.map((dep) => this._loadStylesheetDep(dep)); - return PromiseWrapper.all(promises) - .then((cssTexts) => { - var nestedCompileResultPromises: Promise[] = []; - for (var i = 0; i < result.dependencies.length; i++) { - var dep = result.dependencies[i]; - var cssText = cssTexts[i]; - var nestedCompileResult = - this._styleCompiler.compileStylesheet(dep.moduleUrl, cssText, dep.isShimmed); - nestedCompileResultPromises.push( - this._resolveStylesCompileResult(dep.moduleUrl, nestedCompileResult)); - } - return PromiseWrapper.all(nestedCompileResultPromises); - }) - .then((nestedStylesArr) => { - for (var i = 0; i < result.dependencies.length; i++) { - var dep = result.dependencies[i]; - dep.valuePlaceholder.runtime = nestedStylesArr[i]; - dep.valuePlaceholder.name = `importedStyles${i}`; - } - if (IS_DART || !this._genConfig.useJit) { - return interpretStatements( - result.statements, result.stylesVar, new InterpretiveAppViewInstanceFactory()); - } else { - return jitStatements(`${sourceUrl}.css.js`, result.statements, result.stylesVar); - } - }); - } - - private _loadStylesheetDep(dep: StylesCompileDependency): Promise { - var cacheKey = `${dep.moduleUrl}${dep.isShimmed ? '.shim' : ''}`; - var cssTextPromise = this._styleCache.get(cacheKey); - if (isBlank(cssTextPromise)) { - cssTextPromise = this._xhr.get(dep.moduleUrl); - this._styleCache.set(cacheKey, cssTextPromise); + private _resolveAndEvalStylesCompileResult( + result: CompiledStylesheet, + externalStylesheetsByModuleUrl: Map): string[] { + this._resolveStylesCompileResult(result, externalStylesheetsByModuleUrl); + if (IS_DART || !this._genConfig.useJit) { + return interpretStatements( + result.statements, result.stylesVar, new InterpretiveAppViewInstanceFactory()); + } else { + return jitStatements(`${result.meta.moduleUrl}.css.js`, result.statements, result.stylesVar); } - return cssTextPromise; - } -} - -class CompileHostTemplate { - componentFactory: ComponentFactory; - done: Promise>; - constructor(_template: CompiledTemplate, compMeta: CompileDirectiveMetadata) { - this.componentFactory = new ComponentFactory( - compMeta.selector, _template.proxyViewFactory, compMeta.type.runtime); - this.done = _template.done.then((_) => this.componentFactory); } } class CompiledTemplate { private _viewFactory: Function = null; proxyViewFactory: Function; - constructor(public done: Promise) { - this.proxyViewFactory = - (viewUtils: any /** TODO #9100 */, childInjector: any /** TODO #9100 */, - contextEl: any /** TODO #9100 */) => - this._viewFactory(viewUtils, childInjector, contextEl); + proxyComponentFactory: ComponentFactory; + loading: Promise = null; + private _normalizedCompMeta: CompileDirectiveMetadata = null; + isCompiled = false; + isCompiledWithDeps = false; + + constructor( + public isHost: boolean, selector: string, public compType: CompileIdentifierMetadata, + public viewDirectives: CompileDirectiveMetadata[], public viewComponentTypes: Type[], + public precompileHostComponentTypes: Type[], public viewPipes: CompilePipeMetadata[], + private _normalizeResult: NormalizeDirectiveResult) { + this.proxyViewFactory = (...args: any[]) => this._viewFactory.apply(null, args); + this.proxyComponentFactory = isHost ? + new ComponentFactory(selector, this.proxyViewFactory, compType.runtime) : + null; + if (_normalizeResult.syncResult) { + this._normalizedCompMeta = _normalizeResult.syncResult; + } else { + this.loading = _normalizeResult.asyncResult.then((normalizedCompMeta) => { + this._normalizedCompMeta = normalizedCompMeta; + this.loading = null; + }); + } } - init(viewFactory: Function) { this._viewFactory = viewFactory; } + get normalizedCompMeta(): CompileDirectiveMetadata { + if (this.loading) { + throw new BaseException(`Template is still loading for ${this.compType.name}!`); + } + return this._normalizedCompMeta; + } + + compiled(viewFactory: Function) { + this._viewFactory = viewFactory; + this.isCompiled = true; + } + + depsCompiled() { this.isCompiledWithDeps = true; } } function assertComponent(meta: CompileDirectiveMetadata) { diff --git a/modules/@angular/compiler/src/style_compiler.ts b/modules/@angular/compiler/src/style_compiler.ts index b1b7e2e9cc..955f2da6cd 100644 --- a/modules/@angular/compiler/src/style_compiler.ts +++ b/modules/@angular/compiler/src/style_compiler.ts @@ -10,7 +10,7 @@ import {Injectable, ViewEncapsulation} from '@angular/core'; import {isPresent} from '../src/facade/lang'; -import {CompileDirectiveMetadata, CompileIdentifierMetadata} from './compile_metadata'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileStylesheetMetadata} from './compile_metadata'; import * as o from './output/output_ast'; import {ShadowCss} from './shadow_css'; import {extractStyleUrls} from './style_url_resolver'; @@ -27,9 +27,16 @@ export class StylesCompileDependency { } export class StylesCompileResult { + constructor( + public componentStylesheet: CompiledStylesheet, + public externalStylesheets: CompiledStylesheet[]) {} +} + +export class CompiledStylesheet { constructor( public statements: o.Statement[], public stylesVar: string, - public dependencies: StylesCompileDependency[]) {} + public dependencies: StylesCompileDependency[], public isShimmed: boolean, + public meta: CompileStylesheetMetadata) {} } @Injectable() @@ -40,35 +47,41 @@ export class StyleCompiler { compileComponent(comp: CompileDirectiveMetadata): StylesCompileResult { var shim = comp.template.encapsulation === ViewEncapsulation.Emulated; - return this._compileStyles( - getStylesVarName(comp), comp.template.styles, comp.template.styleUrls, shim); - } - - compileStylesheet(stylesheetUrl: string, cssText: string, isShimmed: boolean): - StylesCompileResult { - var styleWithImports = extractStyleUrls(this._urlResolver, stylesheetUrl, cssText); - return this._compileStyles( - getStylesVarName(null), [styleWithImports.style], styleWithImports.styleUrls, isShimmed); + var externalStylesheets: CompiledStylesheet[] = []; + var componentStylesheet: CompiledStylesheet = this._compileStyles( + comp, new CompileStylesheetMetadata({ + styles: comp.template.styles, + styleUrls: comp.template.styleUrls, + moduleUrl: comp.type.moduleUrl + }), + true); + comp.template.externalStylesheets.forEach((stylesheetMeta) => { + var compiledStylesheet = this._compileStyles(comp, stylesheetMeta, false); + externalStylesheets.push(compiledStylesheet); + }); + return new StylesCompileResult(componentStylesheet, externalStylesheets); } private _compileStyles( - stylesVar: string, plainStyles: string[], absUrls: string[], - shim: boolean): StylesCompileResult { + comp: CompileDirectiveMetadata, stylesheet: CompileStylesheetMetadata, + isComponentStylesheet: boolean): CompiledStylesheet { + var shim = comp.template.encapsulation === ViewEncapsulation.Emulated; var styleExpressions = - plainStyles.map(plainStyle => o.literal(this._shimIfNeeded(plainStyle, shim))); + stylesheet.styles.map(plainStyle => o.literal(this._shimIfNeeded(plainStyle, shim))); var dependencies: StylesCompileDependency[] = []; - for (var i = 0; i < absUrls.length; i++) { + for (var i = 0; i < stylesheet.styleUrls.length; i++) { var identifier = new CompileIdentifierMetadata({name: getStylesVarName(null)}); - dependencies.push(new StylesCompileDependency(absUrls[i], shim, identifier)); + dependencies.push(new StylesCompileDependency(stylesheet.styleUrls[i], shim, identifier)); styleExpressions.push(new o.ExternalExpr(identifier)); } // styles variable contains plain strings and arrays of other styles arrays (recursive), // so we set its type to dynamic. + var stylesVar = getStylesVarName(isComponentStylesheet ? comp : null); var stmt = o.variable(stylesVar) .set(o.literalArr( styleExpressions, new o.ArrayType(o.DYNAMIC_TYPE, [o.TypeModifier.Const]))) .toDeclStmt(null, [o.StmtModifier.Final]); - return new StylesCompileResult([stmt], stylesVar, dependencies); + return new CompiledStylesheet([stmt], stylesVar, dependencies, shim, stylesheet); } private _shimIfNeeded(style: string, shim: boolean): string { @@ -78,7 +91,7 @@ export class StyleCompiler { function getStylesVarName(component: CompileDirectiveMetadata): string { var result = `styles`; - if (isPresent(component)) { + if (component) { result += `_${component.type.name}`; } return result; diff --git a/modules/@angular/compiler/src/view_compiler/view_builder.ts b/modules/@angular/compiler/src/view_compiler/view_builder.ts index 3547724bc3..3f573ccf5c 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -32,7 +32,7 @@ var rootSelectorVar = o.variable('rootSelector'); export class ViewFactoryDependency { constructor( - public comp: CompileDirectiveMetadata, public placeholder: CompileIdentifierMetadata) {} + public comp: CompileIdentifierMetadata, public placeholder: CompileIdentifierMetadata) {} } export class ComponentFactoryDependency { @@ -208,7 +208,8 @@ class ViewBuilderVisitor implements TemplateAstVisitor { if (isPresent(component)) { let nestedComponentIdentifier = new CompileIdentifierMetadata({name: getViewFactoryName(component, 0)}); - this.targetDependencies.push(new ViewFactoryDependency(component, nestedComponentIdentifier)); + this.targetDependencies.push( + new ViewFactoryDependency(component.type, nestedComponentIdentifier)); let precompileComponentIdentifiers = component.precompile.map((precompileComp: CompileIdentifierMetadata) => { var id = new CompileIdentifierMetadata({name: precompileComp.name}); diff --git a/modules/@angular/compiler/src/view_resolver.ts b/modules/@angular/compiler/src/view_resolver.ts index bb3c9bd4d4..2251e58b4d 100644 --- a/modules/@angular/compiler/src/view_resolver.ts +++ b/modules/@angular/compiler/src/view_resolver.ts @@ -19,24 +19,9 @@ import {Map} from '../src/facade/collection'; */ @Injectable() export class ViewResolver { - /** @internal */ - _cache = new Map(); - constructor(private _reflector: ReflectorReader = reflector) {} resolve(component: Type): ViewMetadata { - var view = this._cache.get(component); - - if (isBlank(view)) { - view = this._resolve(component); - this._cache.set(component, view); - } - - return view; - } - - /** @internal */ - _resolve(component: Type): ViewMetadata { var compMeta: ComponentMetadata; this._reflector.annotations(component).forEach(m => { diff --git a/modules/@angular/compiler/test/directive_normalizer_spec.ts b/modules/@angular/compiler/test/directive_normalizer_spec.ts index fb0c6bd63f..a1cc00ca70 100644 --- a/modules/@angular/compiler/test/directive_normalizer_spec.ts +++ b/modules/@angular/compiler/test/directive_normalizer_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata'; +import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata'; import {CompilerConfig} from '@angular/compiler/src/config'; import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer'; import {XHR} from '@angular/compiler/src/xhr'; @@ -15,6 +15,7 @@ import {ViewEncapsulation} from '@angular/core/src/metadata/view'; import {beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; +import {SpyXHR} from './spies'; import {TEST_PROVIDERS} from './test_bindings'; export function main() { @@ -30,176 +31,230 @@ export function main() { new CompileTypeMetadata({moduleUrl: 'http://some/module/a.js', name: 'SomeComp'}); }); - describe('loadTemplate', () => { - describe('inline template', () => { - it('should store the template', - inject( - [AsyncTestCompleter, DirectiveNormalizer], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer) => { - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: 'a', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })) - .then((template: CompileTemplateMetadata) => { - expect(template.template).toEqual('a'); - expect(template.templateUrl).toEqual('package:some/module/a.js'); - async.done(); - }); - })); - - it('should resolve styles on the annotation against the moduleUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer) => { - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })) - .then((template: CompileTemplateMetadata) => { - expect(template.styleUrls).toEqual(['package:some/module/test.css']); - async.done(); - }); - })); - - it('should resolve styles in the template against the moduleUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer) => { - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: [] - })) - .then((template: CompileTemplateMetadata) => { - expect(template.styleUrls).toEqual(['package:some/module/test.css']); - async.done(); - }); - })); - - it('should use ViewEncapsulation.Emulated by default', - inject( - [AsyncTestCompleter, DirectiveNormalizer], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer) => { - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })) - .then((template: CompileTemplateMetadata) => { - expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated); - async.done(); - }); - })); - - it('should use default encapsulation provided by CompilerConfig', - inject( - [AsyncTestCompleter, CompilerConfig, DirectiveNormalizer], - (async: AsyncTestCompleter, config: CompilerConfig, - normalizer: DirectiveNormalizer) => { - config.defaultEncapsulation = ViewEncapsulation.None; - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - encapsulation: null, - template: '', - templateUrl: null, - styles: [], - styleUrls: ['test.css'] - })) - .then((template: CompileTemplateMetadata) => { - expect(template.encapsulation).toEqual(ViewEncapsulation.None); - async.done(); - }); - })); - }); - - describe('templateUrl', () => { - - it('should load a template from a url that is resolved against moduleUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, XHR], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { - xhr.expect('package:some/module/sometplurl.html', 'a'); - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - 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'); - async.done(); - }); - xhr.flush(); - })); - - it('should resolve styles on the annotation against the moduleUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, XHR], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { - xhr.expect('package:some/module/tpl/sometplurl.html', ''); - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - 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(); - }); - xhr.flush(); - })); - - it('should resolve styles in the template against the templateUrl', - inject( - [AsyncTestCompleter, DirectiveNormalizer, XHR], - (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { - xhr.expect( - 'package:some/module/tpl/sometplurl.html', ''); - normalizer - .normalizeTemplate(dirType, new CompileTemplateMetadata({ - 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(); - }); - xhr.flush(); - })); - - }); - + describe('normalizeDirective', () => { it('should throw if no template was specified', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { - expect( - () => normalizer.normalizeTemplate( - dirType, - new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}))) - .toThrowError('No template specified for component SomeComp'); + expect(() => normalizer.normalizeDirective(new CompileDirectiveMetadata({ + type: dirType, + isComponent: true, + template: + new CompileTemplateMetadata({encapsulation: null, styles: [], styleUrls: []}) + }))).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: [] + })) + 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'] + })) + 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: [] + })) + 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'] + })) + expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated); + })); + + it('should use default encapsulation provided by CompilerConfig', + inject( + [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'] + })) + expect(template.encapsulation).toEqual(ViewEncapsulation.None); + })); + }); + + describe('templateUrl', () => { + + it('should load a template from a url that is resolved against moduleUrl', + inject( + [AsyncTestCompleter, DirectiveNormalizer, XHR], + (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { + xhr.expect('package:some/module/sometplurl.html', 'a'); + normalizer + .normalizeTemplateAsync(dirType, new CompileTemplateMetadata({ + 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'); + async.done(); + }); + xhr.flush(); + })); + + it('should resolve styles on the annotation against the moduleUrl', + inject( + [AsyncTestCompleter, DirectiveNormalizer, XHR], + (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { + xhr.expect('package:some/module/tpl/sometplurl.html', ''); + normalizer + .normalizeTemplateAsync(dirType, new CompileTemplateMetadata({ + 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(); + }); + xhr.flush(); + })); + + it('should resolve styles in the template against the templateUrl', + inject( + [AsyncTestCompleter, DirectiveNormalizer, XHR], + (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { + xhr.expect( + 'package:some/module/tpl/sometplurl.html', ''); + normalizer + .normalizeTemplateAsync(dirType, new CompileTemplateMetadata({ + 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(); + }); + xhr.flush(); + })); + + }); + + describe('normalizeExternalStylesheets', () => { + + beforeEachProviders(() => [{provide: XHR, useClass: SpyXHR}]); + + it('should load an external stylesheet', + inject( + [AsyncTestCompleter, DirectiveNormalizer, XHR], + (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: SpyXHR) => { + programXhrSpy(xhr, {'package:some/module/test.css': 'a'}); + normalizer + .normalizeExternalStylesheets(new CompileTemplateMetadata({ + template: '', + templateUrl: '', + styleUrls: ['package:some/module/test.css'] + })) + .then((template: CompileTemplateMetadata) => { + expect(template.externalStylesheets.length).toBe(1); + expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({ + moduleUrl: 'package:some/module/test.css', + styles: ['a'], + styleUrls: [] + })); + async.done(); + }); + })); + + it('should load stylesheets referenced by external stylesheets', + inject( + [AsyncTestCompleter, DirectiveNormalizer, XHR], + (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: SpyXHR) => { + programXhrSpy(xhr, { + 'package:some/module/test.css': 'a@import "test2.css"', + 'package:some/module/test2.css': 'b' + }); + normalizer + .normalizeExternalStylesheets(new CompileTemplateMetadata({ + template: '', + templateUrl: '', + styleUrls: ['package:some/module/test.css'] + })) + .then((template: CompileTemplateMetadata) => { + expect(template.externalStylesheets.length).toBe(2); + expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({ + moduleUrl: 'package:some/module/test.css', + styles: ['a'], + styleUrls: ['package:some/module/test2.css'] + })); + expect(template.externalStylesheets[1]).toEqual(new CompileStylesheetMetadata({ + moduleUrl: 'package:some/module/test2.css', + styles: ['b'], + styleUrls: [] + })); + async.done(); + }); + })); + }); + + describe('caching', () => { + it('should work for templateUrl', + inject( + [AsyncTestCompleter, DirectiveNormalizer, XHR], + (async: AsyncTestCompleter, normalizer: DirectiveNormalizer, xhr: MockXHR) => { + xhr.expect('package:some/module/cmp.html', 'a'); + var templateMeta = new CompileTemplateMetadata({ + templateUrl: 'cmp.html', + }); + Promise + .all([ + normalizer.normalizeTemplateAsync(dirType, templateMeta), + normalizer.normalizeTemplateAsync(dirType, templateMeta) + ]) + .then((templates: CompileTemplateMetadata[]) => { + expect(templates[0].template).toEqual('a'); + expect(templates[1].template).toEqual('a'); + async.done(); + }); + xhr.flush(); + })); }); @@ -369,3 +424,14 @@ export function main() { }); }); } + +function programXhrSpy(spy: SpyXHR, results: {[key: string]: string}) { + spy.spy('get').andCallFake((url: string): Promise => { + var result = results[url]; + if (result) { + return Promise.resolve(result); + } else { + return Promise.reject(`Unknown mock url ${url}`); + } + }); +} \ No newline at end of file diff --git a/modules/@angular/compiler/test/offline_compiler_util.ts b/modules/@angular/compiler/test/offline_compiler_util.ts index 6bab5adb75..81f6f08c12 100644 --- a/modules/@angular/compiler/test/offline_compiler_util.ts +++ b/modules/@angular/compiler/test/offline_compiler_util.ts @@ -20,11 +20,11 @@ import {TemplateParser} from '@angular/compiler/src/template_parser'; import {createOfflineCompileUrlResolver} from '@angular/compiler/src/url_resolver'; import {MODULE_SUFFIX} from '@angular/compiler/src/util'; import {ViewCompiler} from '@angular/compiler/src/view_compiler/view_compiler'; +import {XHR} from '@angular/compiler/src/xhr'; import {Console} from '../core_private'; import {IS_DART, isPresent, print} from '../src/facade/lang'; import {MockSchemaRegistry} from '../testing/schema_registry_mock'; -import {MockXHR} from '../testing/xhr_mock'; export class CompA { user: string; } @@ -44,9 +44,8 @@ export var compAMetadata = CompileDirectiveMetadata.create({ }) }); -function _createOfflineCompiler(xhr: MockXHR, emitter: OutputEmitter): OfflineCompiler { +function _createOfflineCompiler(xhr: XHR, emitter: OutputEmitter): OfflineCompiler { var urlResolver = createOfflineCompileUrlResolver(); - xhr.when(`${THIS_MODULE_PATH}/offline_compiler_compa.html`, 'Hello World {{user}}!'); var htmlParser = new HtmlParser(); var config = new CompilerConfig({genDebugInfo: true, useJit: true}); var normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser, config); @@ -54,24 +53,25 @@ function _createOfflineCompiler(xhr: MockXHR, emitter: OutputEmitter): OfflineCo normalizer, new TemplateParser( new Parser(new Lexer()), new MockSchemaRegistry({}, {}), htmlParser, new Console(), []), - new StyleCompiler(urlResolver), new ViewCompiler(config), emitter, xhr); + new StyleCompiler(urlResolver), new ViewCompiler(config), emitter); } export function compileComp( emitter: OutputEmitter, comp: CompileDirectiveMetadata): Promise { - var xhr = new MockXHR(); + var xhr = new MockXhr(); + xhr.setResult(`${THIS_MODULE_PATH}/offline_compiler_compa.html`, ''); + xhr.setResult(`${THIS_MODULE_PATH}/offline_compiler_compa.css`, ''); var compiler = _createOfflineCompiler(xhr, emitter); var result = compiler.normalizeDirectiveMetadata(comp).then((normComp) => { - return compiler.compileTemplates([new NormalizedComponentWithViewDirectives(normComp, [], [])]) + return compiler + .compileTemplates([new NormalizedComponentWithViewDirectives(normComp, [], [])])[0] .source; }); - xhr.flush(); return result; } export class SimpleJsImportGenerator implements ImportGenerator { getImportPath(moduleUrlStr: string, importedUrlStr: string): string { - // var moduleAssetUrl = ImportGenerator.parseAssetUrl(moduleUrlStr); var importedAssetUrl = ImportGenerator.parseAssetUrl(importedUrlStr); if (isPresent(importedAssetUrl)) { return `${importedAssetUrl.packageName}/${importedAssetUrl.modulePath}`; @@ -80,3 +80,15 @@ export class SimpleJsImportGenerator implements ImportGenerator { } } } + +class MockXhr implements XHR { + results: {[key: string]: string} = {}; + setResult(url: string, content: string) { this.results[url] = content; } + + get(url: string): Promise { + if (url in this.results) { + return Promise.resolve(this.results[url]); + } + return Promise.reject(`Unknown url ${url}`); + } +} \ No newline at end of file diff --git a/modules/@angular/compiler/test/runtime_compiler_spec.ts b/modules/@angular/compiler/test/runtime_compiler_spec.ts new file mode 100644 index 0000000000..2ad77e4485 --- /dev/null +++ b/modules/@angular/compiler/test/runtime_compiler_spec.ts @@ -0,0 +1,108 @@ +import {beforeEach, ddescribe, xdescribe, describe, expect, iit, inject, beforeEachProviders, it, xit,} from '@angular/core/testing/testing_internal'; +import {Injectable, Component, Input, ViewMetadata, Compiler, ComponentFactory, Injector} from '@angular/core'; +import {ConcreteType, stringify} from '../src/facade/lang'; +import {fakeAsync, tick, TestComponentBuilder, ComponentFixture} from '@angular/core/testing'; +import {XHR, ViewResolver} from '@angular/compiler'; +import {MockViewResolver} from '@angular/compiler/testing'; + +import {SpyXHR} from './spies'; + +@Component({selector: 'child-cmp', template: 'childComp'}) +class ChildComp { +} + +@Component({selector: 'some-cmp', template: 'someComp'}) +class SomeComp { +} + +@Component({selector: 'some-cmp', templateUrl: './someTpl'}) +class SomeCompWithUrlTemplate { +} + +export function main() { + describe('RuntimeCompiler', () => { + let compiler: Compiler; + let xhr: SpyXHR; + let tcb: TestComponentBuilder; + let viewResolver: MockViewResolver; + let injector: Injector; + + beforeEachProviders(() => [{provide: XHR, useValue: new SpyXHR()}]); + + beforeEach(inject( + [Compiler, TestComponentBuilder, XHR, ViewResolver, Injector], + (_compiler: Compiler, _tcb: TestComponentBuilder, _xhr: SpyXHR, + _viewResolver: MockViewResolver, _injector: Injector) => { + compiler = _compiler; + tcb = _tcb; + xhr = _xhr; + viewResolver = _viewResolver; + injector = _injector; + })); + + describe('clearCacheFor', () => { + it('should support changing the content of a template referenced via templateUrl', + fakeAsync(() => { + xhr.spy('get').andCallFake(() => Promise.resolve('init')); + let compFixture = + tcb.overrideView(SomeComp, new ViewMetadata({templateUrl: '/myComp.html'})) + .createFakeAsync(SomeComp); + expect(compFixture.nativeElement).toHaveText('init'); + + xhr.spy('get').andCallFake(() => Promise.resolve('new content')); + // Note: overrideView is calling .clearCacheFor... + compFixture = tcb.overrideView(SomeComp, new ViewMetadata({templateUrl: '/myComp.html'})) + .createFakeAsync(SomeComp); + expect(compFixture.nativeElement).toHaveText('new content'); + })); + + it('should support overwriting inline templates', () => { + let componentFixture = tcb.createSync(SomeComp); + expect(componentFixture.nativeElement).toHaveText('someComp'); + + componentFixture = tcb.overrideTemplate(SomeComp, 'test').createSync(SomeComp); + expect(componentFixture.nativeElement).toHaveText('test'); + }); + + it('should not update existing compilation results', () => { + viewResolver.setView( + SomeComp, + new ViewMetadata({template: '', directives: [ChildComp]})); + viewResolver.setInlineTemplate(ChildComp, 'oldChild'); + let compFactory = compiler.compileComponentSync(SomeComp); + viewResolver.setInlineTemplate(ChildComp, 'newChild'); + compiler.compileComponentSync(SomeComp); + let compRef = compFactory.create(injector); + expect(compRef.location.nativeElement).toHaveText('oldChild'); + }); + }); + + describe('compileComponentSync', () => { + it('should throw when using a templateUrl that has not been compiled before', () => { + xhr.spy('get').andCallFake(() => Promise.resolve('')); + expect(() => tcb.createSync(SomeCompWithUrlTemplate)) + .toThrowError( + `Can't compile synchronously as ${stringify(SomeCompWithUrlTemplate)} is still being loaded!`); + }); + + it('should throw when using a templateUrl in a nested component that has not been compiled before', + () => { + xhr.spy('get').andCallFake(() => Promise.resolve('')); + let localTcb = + tcb.overrideView(SomeComp, new ViewMetadata({template: '', directives: [ChildComp]})) + .overrideView(ChildComp, new ViewMetadata({templateUrl: '/someTpl.html'})); + expect(() => localTcb.createSync(SomeComp)) + .toThrowError( + `Can't compile synchronously as ${stringify(ChildComp)} is still being loaded!`); + }); + + it('should allow to use templateUrl components that have been loaded before', + fakeAsync(() => { + xhr.spy('get').andCallFake(() => Promise.resolve('hello')); + tcb.createFakeAsync(SomeCompWithUrlTemplate); + let compFixture = tcb.createSync(SomeCompWithUrlTemplate); + expect(compFixture.nativeElement).toHaveText('hello'); + })); + }); + }); +} \ No newline at end of file diff --git a/modules/@angular/compiler/test/test_component_builder_spec.ts b/modules/@angular/compiler/test/test_component_builder_spec.ts index e47705dfa5..33f58f0880 100644 --- a/modules/@angular/compiler/test/test_component_builder_spec.ts +++ b/modules/@angular/compiler/test/test_component_builder_spec.ts @@ -9,7 +9,7 @@ import {beforeEach, ddescribe, xdescribe, describe, expect, iit, inject, beforeEachProviders, it, xit,} from '@angular/core/testing/testing_internal'; import {TestComponentBuilder, ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from '@angular/compiler/testing'; import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; -import {Injectable, Component, Input, ViewMetadata, ComponentResolver} from '@angular/core'; +import {Injectable, Component, Input, ViewMetadata} from '@angular/core'; import {NgIf} from '@angular/common'; import {TimerWrapper} from '../src/facade/async'; import {IS_DART} from '../src/facade/lang'; @@ -320,6 +320,15 @@ export function main() { }); })); + it('should create components synchronously', + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + let componentFixture = + tcb.overrideTemplate(MockChildComp, 'Mock').createSync(MockChildComp); + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('Mock'); + })); + if (!IS_DART) { describe('ComponentFixture', () => { it('should auto detect changes if autoDetectChanges is called', @@ -604,27 +613,6 @@ export function main() { })); }); - describe('createSync', () => { - it('should create components', - inject( - [ComponentResolver, TestComponentBuilder, AsyncTestCompleter], - (cr: ComponentResolver, tcb: TestComponentBuilder, async: AsyncTestCompleter) => { - cr.resolveComponent(MyIfComp).then((cmpFactory) => { - let componentFixture = tcb.createSync(cmpFactory); - - componentFixture.detectChanges(); - expect(componentFixture.nativeElement).toHaveText('MyIf()'); - - componentFixture.componentInstance.showMore = true; - componentFixture.detectChanges(); - expect(componentFixture.nativeElement).toHaveText('MyIf(More)'); - - async.done(); - }); - })); - - }); - }); } }); diff --git a/modules/@angular/compiler/test/view_resolver_mock_spec.ts b/modules/@angular/compiler/test/view_resolver_mock_spec.ts index 9a266687b2..1a602768f6 100644 --- a/modules/@angular/compiler/test/view_resolver_mock_spec.ts +++ b/modules/@angular/compiler/test/view_resolver_mock_spec.ts @@ -6,18 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {beforeEach, ddescribe, describe, expect, iit, it,} from '@angular/core/testing/testing_internal'; +import {beforeEach, ddescribe, describe, expect, iit, it, inject,} from '@angular/core/testing/testing_internal'; import {stringify} from '../src/facade/lang'; import {MockViewResolver} from '../testing'; -import {Component, ViewMetadata} from '@angular/core'; +import {Component, ViewMetadata, Injector} from '@angular/core'; import {isBlank} from '../src/facade/lang'; export function main() { describe('MockViewResolver', () => { var viewResolver: MockViewResolver; - beforeEach(() => { viewResolver = new MockViewResolver(); }); + beforeEach(inject( + [Injector], (injector: Injector) => { viewResolver = new MockViewResolver(injector); })); describe('View overriding', () => { it('should fallback to the default ViewResolver when templates are not overridden', () => { @@ -33,13 +34,12 @@ export function main() { expect(isBlank(view.directives)).toBe(true); }); - it('should not allow overriding a view after it has been resolved', () => { + it('should allow overriding a view after it has been resolved', () => { viewResolver.resolve(SomeComponent); - expect(() => { - viewResolver.setView(SomeComponent, new ViewMetadata({template: 'overridden template'})); - }) - .toThrowError( - `The component ${stringify(SomeComponent)} has already been compiled, its configuration can not be changed`); + viewResolver.setView(SomeComponent, new ViewMetadata({template: 'overridden template'})); + var view = viewResolver.resolve(SomeComponent); + expect(view.template).toEqual('overridden template'); + expect(isBlank(view.directives)).toBe(true); }); }); @@ -58,11 +58,11 @@ export function main() { expect(view.template).toEqual('overridden template x 2'); }); - it('should not allow overriding a view after it has been resolved', () => { + it('should allow overriding a view after it has been resolved', () => { viewResolver.resolve(SomeComponent); - expect(() => { viewResolver.setInlineTemplate(SomeComponent, 'overridden template'); }) - .toThrowError( - `The component ${stringify(SomeComponent)} has already been compiled, its configuration can not be changed`); + viewResolver.setInlineTemplate(SomeComponent, 'overridden template'); + var view = viewResolver.resolve(SomeComponent); + expect(view.template).toEqual('overridden template'); }); }); @@ -90,13 +90,12 @@ export function main() { `Overriden directive ${stringify(SomeOtherDirective)} not found in the template of ${stringify(SomeComponent)}`); }); - it('should not allow overriding a directive after its view has been resolved', () => { + it('should allow overriding a directive after its view has been resolved', () => { viewResolver.resolve(SomeComponent); - expect(() => { - viewResolver.overrideViewDirective(SomeComponent, SomeDirective, SomeOtherDirective); - }) - .toThrowError( - `The component ${stringify(SomeComponent)} has already been compiled, its configuration can not be changed`); + viewResolver.overrideViewDirective(SomeComponent, SomeDirective, SomeOtherDirective); + var view = viewResolver.resolve(SomeComponent); + expect(view.directives.length).toEqual(1); + expect(view.directives[0]).toBe(SomeOtherDirective); }); }); }); diff --git a/modules/@angular/compiler/testing/directive_resolver_mock.ts b/modules/@angular/compiler/testing/directive_resolver_mock.ts index edb682a711..9a764f0e27 100644 --- a/modules/@angular/compiler/testing/directive_resolver_mock.ts +++ b/modules/@angular/compiler/testing/directive_resolver_mock.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentMetadata, DirectiveMetadata, Injectable} from '@angular/core'; +import {Compiler, ComponentMetadata, DirectiveMetadata, Injectable, Injector} from '@angular/core'; import {DirectiveResolver} from '../src/directive_resolver'; import {Map} from '../src/facade/collection'; import {Type, isPresent} from '../src/facade/lang'; + /** * An implementation of {@link DirectiveResolver} that allows overriding * various properties of directives. @@ -22,6 +23,10 @@ export class MockDirectiveResolver extends DirectiveResolver { private _providerOverrides = new Map(); private viewProviderOverrides = new Map(); + constructor(private _injector: Injector) { super(); } + + private get _compiler(): Compiler { return this._injector.get(Compiler); } + resolve(type: Type): DirectiveMetadata { var dm = super.resolve(type); @@ -69,9 +74,11 @@ export class MockDirectiveResolver extends DirectiveResolver { setProvidersOverride(type: Type, providers: any[]): void { this._providerOverrides.set(type, providers); + this._compiler.clearCacheFor(type); } setViewProvidersOverride(type: Type, viewProviders: any[]): void { this.viewProviderOverrides.set(type, viewProviders); + this._compiler.clearCacheFor(type); } } diff --git a/modules/@angular/compiler/testing/test_component_builder.ts b/modules/@angular/compiler/testing/test_component_builder.ts index 87c6995dc6..72a470af7c 100644 --- a/modules/@angular/compiler/testing/test_component_builder.ts +++ b/modules/@angular/compiler/testing/test_component_builder.ts @@ -6,39 +6,36 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationEntryMetadata, ComponentFactory, ComponentResolver, Injectable, Injector, NgZone, ViewMetadata} from '@angular/core'; +import {AnimationEntryMetadata, Compiler, ComponentFactory, Injectable, Injector, NgZone, ViewMetadata} from '@angular/core'; import {ComponentFixture, ComponentFixtureNoNgZone, TestComponentBuilder} from '@angular/core/testing'; import {DirectiveResolver, ViewResolver} from '../index'; import {MapWrapper} from '../src/facade/collection'; -import {IS_DART, Type, isPresent} from '../src/facade/lang'; +import {ConcreteType, IS_DART, Type, isPresent} from '../src/facade/lang'; /** * @deprecated Import TestComponentRenderer from @angular/core/testing */ export {TestComponentRenderer} from '@angular/core/testing'; - /** * @deprecated Import TestComponentBuilder from @angular/core/testing */ export {TestComponentBuilder} from '@angular/core/testing'; - /** * @deprecated Import ComponentFixture from @angular/core/testing */ export {ComponentFixture} from '@angular/core/testing'; - /** * @deprecated Import ComponentFixtureNoNgZone from @angular/core/testing */ export {ComponentFixtureNoNgZone} from '@angular/core/testing'; - /** * @deprecated Import ComponentFixtureAutoDetect from @angular/core/testing */ export {ComponentFixtureAutoDetect} from '@angular/core/testing'; + /** * A TestComponentBuilder that allows overriding based on the compiler. */ @@ -114,32 +111,31 @@ export class OverridingTestComponentBuilder extends TestComponentBuilder { return clone; } - createAsync(rootComponentType: Type): Promise> { - let noNgZone = IS_DART || this._injector.get(ComponentFixtureNoNgZone, false); - let ngZone: NgZone = noNgZone ? null : this._injector.get(NgZone, null); + createAsync(rootComponentType: ConcreteType): Promise> { + this._applyMetadataOverrides(); + return super.createAsync(rootComponentType); + } - let initComponent = () => { - let mockDirectiveResolver = this._injector.get(DirectiveResolver); - let mockViewResolver = this._injector.get(ViewResolver); - this._viewOverrides.forEach((view, type) => mockViewResolver.setView(type, view)); - this._templateOverrides.forEach( - (template, type) => mockViewResolver.setInlineTemplate(type, template)); - this._animationOverrides.forEach( - (animationsEntry, type) => mockViewResolver.setAnimations(type, animationsEntry)); - this._directiveOverrides.forEach((overrides, component) => { - overrides.forEach( - (to, from) => { mockViewResolver.overrideViewDirective(component, from, to); }); - }); - this._bindingsOverrides.forEach( - (bindings, type) => mockDirectiveResolver.setProvidersOverride(type, bindings)); - this._viewBindingsOverrides.forEach( - (bindings, type) => mockDirectiveResolver.setViewProvidersOverride(type, bindings)); + createSync(rootComponentType: ConcreteType): ComponentFixture { + this._applyMetadataOverrides(); + return super.createSync(rootComponentType); + } - let promise: Promise> = - this._injector.get(ComponentResolver).resolveComponent(rootComponentType); - return promise.then(componentFactory => this.createFromFactory(ngZone, componentFactory)); - }; - - return ngZone == null ? initComponent() : ngZone.run(initComponent); + private _applyMetadataOverrides() { + let mockDirectiveResolver = this._injector.get(DirectiveResolver); + let mockViewResolver = this._injector.get(ViewResolver); + this._viewOverrides.forEach((view, type) => { mockViewResolver.setView(type, view); }); + this._templateOverrides.forEach( + (template, type) => mockViewResolver.setInlineTemplate(type, template)); + this._animationOverrides.forEach( + (animationsEntry, type) => mockViewResolver.setAnimations(type, animationsEntry)); + this._directiveOverrides.forEach((overrides, component) => { + overrides.forEach( + (to, from) => { mockViewResolver.overrideViewDirective(component, from, to); }); + }); + this._bindingsOverrides.forEach( + (bindings, type) => mockDirectiveResolver.setProvidersOverride(type, bindings)); + this._viewBindingsOverrides.forEach( + (bindings, type) => mockDirectiveResolver.setViewProvidersOverride(type, bindings)); } } diff --git a/modules/@angular/compiler/testing/view_resolver_mock.ts b/modules/@angular/compiler/testing/view_resolver_mock.ts index 376b378b6d..4b7b5090c1 100644 --- a/modules/@angular/compiler/testing/view_resolver_mock.ts +++ b/modules/@angular/compiler/testing/view_resolver_mock.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationEntryMetadata, BaseException, Injectable, Type, ViewMetadata, resolveForwardRef} from '@angular/core'; +import {AnimationEntryMetadata, BaseException, Compiler, Injectable, Injector, Type, ViewMetadata, resolveForwardRef} from '@angular/core'; import {ViewResolver} from '../index'; import {Map} from '../src/facade/collection'; @@ -21,38 +21,38 @@ export class MockViewResolver extends ViewResolver { /** @internal */ _animations = new Map(); /** @internal */ - _viewCache = new Map(); - /** @internal */ _directiveOverrides = new Map>(); - constructor() { super(); } + constructor(private _injector: Injector) { super(); } + + private get _compiler(): Compiler { return this._injector.get(Compiler); } + + private _clearCacheFor(component: Type) { this._compiler.clearCacheFor(component); } /** * Overrides the {@link ViewMetadata} for a component. */ setView(component: Type, view: ViewMetadata): void { - this._checkOverrideable(component); this._views.set(component, view); + this._clearCacheFor(component); } /** * Overrides the inline template for a component - other configuration remains unchanged. */ setInlineTemplate(component: Type, template: string): void { - this._checkOverrideable(component); this._inlineTemplates.set(component, template); + this._clearCacheFor(component); } setAnimations(component: Type, animations: AnimationEntryMetadata[]): void { - this._checkOverrideable(component); this._animations.set(component, animations); + this._clearCacheFor(component); } /** * Overrides a directive from the component {@link ViewMetadata}. */ overrideViewDirective(component: Type, from: Type, to: Type): void { - this._checkOverrideable(component); - var overrides = this._directiveOverrides.get(component); if (isBlank(overrides)) { @@ -61,6 +61,7 @@ export class MockViewResolver extends ViewResolver { } overrides.set(from, to); + this._clearCacheFor(component); } /** @@ -72,10 +73,7 @@ export class MockViewResolver extends ViewResolver { * - Override the @View definition, see `setInlineTemplate`. */ resolve(component: Type): ViewMetadata { - var view = this._viewCache.get(component); - if (isPresent(view)) return view; - - view = this._views.get(component); + var view = this._views.get(component); if (isBlank(view)) { view = super.resolve(component); } @@ -123,26 +121,8 @@ export class MockViewResolver extends ViewResolver { interpolation: view.interpolation }); - this._viewCache.set(component, view); return view; } - - /** - * @internal - * - * Once a component has been compiled, the AppProtoView is stored in the compiler cache. - * - * Then it should not be possible to override the component configuration after the component - * has been compiled. - */ - _checkOverrideable(component: Type): void { - var cached = this._viewCache.get(component); - - if (isPresent(cached)) { - throw new BaseException( - `The component ${stringify(component)} has already been compiled, its configuration can not be changed`); - } - } } function flattenArray(tree: any[], out: Array): void { diff --git a/modules/@angular/core/src/linker.ts b/modules/@angular/core/src/linker.ts index 640e932c4e..263176f1d7 100644 --- a/modules/@angular/core/src/linker.ts +++ b/modules/@angular/core/src/linker.ts @@ -7,6 +7,7 @@ */ // Public API for compiler +export {Compiler} from './linker/compiler'; export {ComponentFactory, ComponentRef} from './linker/component_factory'; export {ComponentFactoryResolver, NoComponentFactoryError} from './linker/component_factory_resolver'; export {ComponentResolver} from './linker/component_resolver'; diff --git a/modules/@angular/core/src/linker/compiler.ts b/modules/@angular/core/src/linker/compiler.ts new file mode 100644 index 0000000000..1aa89828e6 --- /dev/null +++ b/modules/@angular/core/src/linker/compiler.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {BaseException} from '../facade/exceptions'; +import {ConcreteType, Type, stringify} from '../facade/lang'; + +import {ComponentFactory} from './component_factory'; + + +/** + * Low-level service for running the angular compiler duirng runtime + * to create {@link ComponentFactory}s, which + * can later be used to create and render a Component instance. + * @stable + */ +export class Compiler { + /** + * Loads the template and styles of a component and returns the associated `ComponentFactory`. + */ + compileComponentAsync(component: ConcreteType): Promise> { + throw new BaseException( + `Runtime compiler is not loaded. Tried to compile ${stringify(component)}`); + } + /** + * Compiles the given component. All templates have to be either inline or compiled via + * `compileComponentAsync` before. + */ + compileComponentSync(component: ConcreteType): ComponentFactory { + throw new BaseException( + `Runtime compiler is not loaded. Tried to compile ${stringify(component)}`); + } + /** + * Clears all caches + */ + clearCache(): void {} + /** + * Clears the cache for the given component. + */ + clearCacheFor(compType: Type) {} +} diff --git a/modules/@angular/core/src/linker/component_factory_resolver.ts b/modules/@angular/core/src/linker/component_factory_resolver.ts index 969f1786ed..5c27dd0079 100644 --- a/modules/@angular/core/src/linker/component_factory_resolver.ts +++ b/modules/@angular/core/src/linker/component_factory_resolver.ts @@ -8,7 +8,7 @@ import {Inject, OpaqueToken, Optional, SkipSelf} from '../di'; import {BaseException} from '../facade/exceptions'; -import {ClassWithConstructor, stringify} from '../facade/lang'; +import {ConcreteType, stringify} from '../facade/lang'; import {ComponentFactory} from './component_factory'; @@ -33,7 +33,7 @@ class _NullComponentFactoryResolver implements ComponentFactoryResolver { */ export abstract class ComponentFactoryResolver { static NULL: ComponentFactoryResolver = new _NullComponentFactoryResolver(); - abstract resolveComponentFactory(component: ClassWithConstructor): ComponentFactory; + abstract resolveComponentFactory(component: ConcreteType): ComponentFactory; } export class CodegenComponentFactoryResolver implements ComponentFactoryResolver { diff --git a/modules/@angular/core/src/reflection/reflection_capabilities.ts b/modules/@angular/core/src/reflection/reflection_capabilities.ts index 387f116931..67322e192b 100644 --- a/modules/@angular/core/src/reflection/reflection_capabilities.ts +++ b/modules/@angular/core/src/reflection/reflection_capabilities.ts @@ -19,7 +19,7 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { isReflectionEnabled(): boolean { return true; } - factory(t: ConcreteType): Function { + factory(t: ConcreteType): Function { switch (t.length) { case 0: return () => new t(); diff --git a/modules/@angular/core/src/util/decorators.ts b/modules/@angular/core/src/util/decorators.ts index ad9d7a0fa3..40adba0515 100644 --- a/modules/@angular/core/src/util/decorators.ts +++ b/modules/@angular/core/src/util/decorators.ts @@ -84,7 +84,7 @@ export interface TypeDecorator { /** * Generate a class from the definition and annotate it with {@link TypeDecorator#annotations}. */ - Class(obj: ClassDefinition): ConcreteType; + Class(obj: ClassDefinition): ConcreteType; } function extractAnnotation(annotation: any): any { @@ -219,7 +219,7 @@ function applyParams(fnOrArray: (Function | any[]), key: string): Function { * ``` * @stable */ -export function Class(clsDef: ClassDefinition): ConcreteType { +export function Class(clsDef: ClassDefinition): ConcreteType { var constructor = applyParams( clsDef.hasOwnProperty('constructor') ? clsDef.constructor : undefined, 'constructor'); var proto = constructor.prototype; @@ -246,7 +246,7 @@ export function Class(clsDef: ClassDefinition): ConcreteType { (constructor as any /** TODO #9100 */)['overriddenName'] = `class${_nextClassId++}`; } - return constructor; + return >constructor; } var Reflect = global.Reflect; diff --git a/modules/@angular/core/test/linker/change_detection_integration_spec.ts b/modules/@angular/core/test/linker/change_detection_integration_spec.ts index 91ee35d204..7e566e18ed 100644 --- a/modules/@angular/core/test/linker/change_detection_integration_spec.ts +++ b/modules/@angular/core/test/linker/change_detection_integration_spec.ts @@ -10,7 +10,7 @@ import {TestComponentBuilder} from '@angular/compiler/testing'; import {ComponentFixture, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} from '@angular/core/testing/testing_internal'; -import {isBlank, NumberWrapper,} from '../../src/facade/lang'; +import {isBlank, NumberWrapper, ConcreteType,} from '../../src/facade/lang'; import {BaseException} from '../../src/facade/exceptions'; import {StringMapWrapper} from '../../src/facade/collection'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -40,9 +40,9 @@ export function main() { var renderLog: RenderLog; var directiveLog: DirectiveLog; - function createCompFixture( - template: string, compType: Type = TestComponent, - _tcb: TestComponentBuilder = null): ComponentFixture { + function createCompFixture( + template: string, compType: ConcreteType = TestComponent, + _tcb: TestComponentBuilder = null): ComponentFixture { if (isBlank(_tcb)) { _tcb = tcb; } @@ -58,18 +58,19 @@ export function main() { return nodes.map(node => node.injector.get(dirType)); } - function _bindSimpleProp( - bindAttr: string, compType: Type = TestComponent): ComponentFixture { + function _bindSimpleProp( + bindAttr: string, compType: ConcreteType = TestComponent): ComponentFixture { var template = `
`; return createCompFixture(template, compType); } - function _bindSimpleValue( - expression: any, compType: Type = TestComponent): ComponentFixture { + function _bindSimpleValue( + expression: any, compType: ConcreteType = TestComponent): ComponentFixture { return _bindSimpleProp(`[someProp]='${expression}'`, compType); } - function _bindAndCheckSimpleValue(expression: any, compType: Type = TestComponent): string[] { + function _bindAndCheckSimpleValue( + expression: any, compType: ConcreteType = TestComponent): string[] { var ctx = _bindSimpleValue(expression, compType); ctx.detectChanges(false); return renderLog.log; diff --git a/modules/@angular/core/test/linker/integration_spec.ts b/modules/@angular/core/test/linker/integration_spec.ts index c5cb00ff3e..3956f90c10 100644 --- a/modules/@angular/core/test/linker/integration_spec.ts +++ b/modules/@angular/core/test/linker/integration_spec.ts @@ -1504,20 +1504,15 @@ function declareTests({useJit}: {useJit: boolean}) { describe('error handling', () => { it('should report a meaningful error when a directive is missing annotation', - inject( - [TestComponentBuilder, AsyncTestCompleter], - (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { - tcb = tcb.overrideView( - MyComp, - new ViewMetadata({template: '', directives: [SomeDirectiveMissingAnnotation]})); + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + tcb = tcb.overrideView( + MyComp, + new ViewMetadata({template: '', directives: [SomeDirectiveMissingAnnotation]})); - PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => { - expect(e.message).toEqual( - `No Directive annotation found on ${stringify(SomeDirectiveMissingAnnotation)}`); - async.done(); - return null; - }); - })); + expect(() => tcb.createAsync(MyComp)) + .toThrowError( + `No Directive annotation found on ${stringify(SomeDirectiveMissingAnnotation)}`); + })); it('should report a meaningful error when a component is missing view annotation', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { @@ -1530,19 +1525,13 @@ function declareTests({useJit}: {useJit: boolean}) { })); it('should report a meaningful error when a directive is null', - inject( - [TestComponentBuilder, AsyncTestCompleter], - (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { - tcb = - tcb.overrideView(MyComp, new ViewMetadata({directives: [[null]], template: ''})); + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + tcb = tcb.overrideView(MyComp, new ViewMetadata({directives: [[null]], template: ''})); - PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => { - expect(e.message).toEqual( - `Unexpected directive value 'null' on the View of component '${stringify(MyComp)}'`); - async.done(); - return null; - }); - })); + expect(() => tcb.createAsync(MyComp)) + .toThrowError( + `Unexpected directive value 'null' on the View of component '${stringify(MyComp)}'`); + })); it('should provide an error context when an error happens in DI', inject( @@ -1642,22 +1631,17 @@ function declareTests({useJit}: {useJit: boolean}) { if (!IS_DART) { it('should report a meaningful error when a directive is undefined', - inject( - [TestComponentBuilder, AsyncTestCompleter], - (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - var undefinedValue: any = void(0); + var undefinedValue: any = void(0); - tcb = tcb.overrideView( - MyComp, new ViewMetadata({directives: [undefinedValue], template: ''})); + tcb = tcb.overrideView( + MyComp, new ViewMetadata({directives: [undefinedValue], template: ''})); - PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => { - expect(e.message).toEqual( - `Unexpected directive value 'undefined' on the View of component '${stringify(MyComp)}'`); - async.done(); - return null; - }); - })); + expect(() => tcb.createAsync(MyComp)) + .toThrowError( + `Unexpected directive value 'undefined' on the View of component '${stringify(MyComp)}'`); + })); } it('should specify a location of an error that happened during change detection (text)', diff --git a/modules/@angular/core/test/linker/query_integration_spec.ts b/modules/@angular/core/test/linker/query_integration_spec.ts index 83da730ca8..92895173fe 100644 --- a/modules/@angular/core/test/linker/query_integration_spec.ts +++ b/modules/@angular/core/test/linker/query_integration_spec.ts @@ -261,18 +261,14 @@ export function main() { })); it('should throw with descriptive error when query selectors are not present', - inject( - [TestComponentBuilder, AsyncTestCompleter], - (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { - tcb.overrideTemplate( - MyCompBroken0, '') - .createAsync(MyCompBroken0) - .catch((e) => { - expect(e.message).toEqual( - `Can't construct a query for the property "errorTrigger" of "${stringify(HasNullQueryCondition)}" since the query selector wasn't defined.`); - async.done(); - }); - })); + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + expect( + () => tcb.overrideTemplate( + MyCompBroken0, '') + .createAsync(MyCompBroken0)) + .toThrowError( + `Can't construct a query for the property "errorTrigger" of "${stringify(HasNullQueryCondition)}" since the query selector wasn't defined.`); + })); }); describe('query for TemplateRef', () => { diff --git a/modules/@angular/core/test/linker/regression_integration_spec.ts b/modules/@angular/core/test/linker/regression_integration_spec.ts index 99c2885563..5799aa5269 100644 --- a/modules/@angular/core/test/linker/regression_integration_spec.ts +++ b/modules/@angular/core/test/linker/regression_integration_spec.ts @@ -12,7 +12,7 @@ import {AsyncTestCompleter} from '@angular/core/testing/testing_internal'; import {IS_DART} from '../../src/facade/lang'; -import {Component, Pipe, PipeTransform, provide, ViewMetadata, OpaqueToken, Injector} from '@angular/core'; +import {Component, Pipe, PipeTransform, provide, ViewMetadata, PLATFORM_PIPES, OpaqueToken, Injector, forwardRef} from '@angular/core'; import {NgIf, NgClass} from '@angular/common'; import {CompilerConfig} from '@angular/compiler'; @@ -170,8 +170,7 @@ function declareTests({useJit}: {useJit: boolean}) { it('should support ngClass before a component and content projection inside of an ngIf', inject( - [TestComponentBuilder, AsyncTestCompleter], - (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: any) => { tcb.overrideView( MyComp1, new ViewMetadata({ template: `ABC`, @@ -185,6 +184,15 @@ function declareTests({useJit}: {useJit: boolean}) { }); })); + it('should handle mutual recursion entered from multiple sides - #7084', + inject( + [TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async: any) => { + tcb.createAsync(FakeRecursiveComp).then((fixture) => { + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('[]'); + async.done(); + }); + })); }); } @@ -207,3 +215,37 @@ class CustomPipe implements PipeTransform { @Component({selector: 'cmp-content', template: ``}) class CmpWithNgContent { } + +@Component({ + selector: 'left', + template: `L`, + directives: [ + NgIf, + forwardRef(() => RightComp), + ] +}) +class LeftComp { +} + +@Component({ + selector: 'right', + template: `R`, + directives: [ + NgIf, + forwardRef(() => LeftComp), + ] +}) +class RightComp { +} + +@Component({ + selector: 'fakeRecursiveComp', + template: `[]`, + directives: [ + NgIf, + forwardRef(() => LeftComp), + forwardRef(() => RightComp), + ] +}) +export class FakeRecursiveComp { +} diff --git a/modules/@angular/core/test/linker/view_injector_integration_spec.ts b/modules/@angular/core/test/linker/view_injector_integration_spec.ts index 7cea7aa417..21be3a960b 100644 --- a/modules/@angular/core/test/linker/view_injector_integration_spec.ts +++ b/modules/@angular/core/test/linker/view_injector_integration_spec.ts @@ -9,7 +9,7 @@ import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, beforeEachProviders, inject,} from '@angular/core/testing/testing_internal'; import {fakeAsync, flushMicrotasks, tick, ComponentFixture} from '@angular/core/testing'; import {TestComponentBuilder} from '@angular/compiler/testing'; -import {isBlank} from '../../src/facade/lang'; +import {isBlank, ConcreteType} from '../../src/facade/lang'; import {Type, ViewContainerRef, TemplateRef, ElementRef, ChangeDetectorRef, ChangeDetectionStrategy, Directive, Component, DebugElement, forwardRef, Input, PipeTransform, Attribute, ViewMetadata, provide, Optional, Inject, Self, InjectMetadata, Pipe, Host, SkipSelfMetadata} from '@angular/core'; import {NgIf, NgFor} from '@angular/common'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -242,10 +242,11 @@ class TestComp { export function main() { var tcb: TestComponentBuilder; - function createCompFixture( - template: string, tcb: TestComponentBuilder, comp: Type = null): ComponentFixture { + function createCompFixture( + template: string, tcb: TestComponentBuilder, + comp: ConcreteType = null): ComponentFixture { if (isBlank(comp)) { - comp = TestComp; + comp = TestComp; } return tcb .overrideView( @@ -255,7 +256,7 @@ export function main() { } function createComp( - template: string, tcb: TestComponentBuilder, comp: Type = null): DebugElement { + template: string, tcb: TestComponentBuilder, comp: ConcreteType = null): DebugElement { var fixture = createCompFixture(template, tcb, comp); fixture.detectChanges(); return fixture.debugElement; diff --git a/modules/@angular/core/testing/test_component_builder.ts b/modules/@angular/core/testing/test_component_builder.ts index a28ec3770c..81747adfc6 100644 --- a/modules/@angular/core/testing/test_component_builder.ts +++ b/modules/@angular/core/testing/test_component_builder.ts @@ -6,14 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationEntryMetadata, ComponentFactory, ComponentResolver, Injectable, Injector, NgZone, OpaqueToken, ViewMetadata} from '../index'; +import {AnimationEntryMetadata, Compiler, ComponentFactory, Injectable, Injector, NgZone, OpaqueToken, ViewMetadata} from '../index'; import {PromiseWrapper} from '../src/facade/async'; -import {IS_DART, Type, isPresent} from '../src/facade/lang'; +import {ConcreteType, IS_DART, Type, isPresent} from '../src/facade/lang'; import {ComponentFixture} from './component_fixture'; import {tick} from './fake_async'; + /** * An abstract class for inserting the root test component element in a platform independent way. * @@ -118,20 +119,21 @@ export class TestComponentBuilder { /** * Builds and returns a ComponentFixture. */ - createAsync(rootComponentType: Type): Promise> { + createAsync(rootComponentType: ConcreteType): Promise> { let noNgZone = IS_DART || this._injector.get(ComponentFixtureNoNgZone, false); let ngZone: NgZone = noNgZone ? null : this._injector.get(NgZone, null); + let compiler: Compiler = this._injector.get(Compiler); let initComponent = () => { let promise: Promise> = - this._injector.get(ComponentResolver).resolveComponent(rootComponentType); + compiler.compileComponentAsync(rootComponentType); return promise.then(componentFactory => this.createFromFactory(ngZone, componentFactory)); }; return ngZone == null ? initComponent() : ngZone.run(initComponent); } - createFakeAsync(rootComponentType: Type): ComponentFixture { + createFakeAsync(rootComponentType: ConcreteType): ComponentFixture { let result: any /** TODO #9100 */; let error: any /** TODO #9100 */; PromiseWrapper.then( @@ -144,15 +146,16 @@ export class TestComponentBuilder { return result; } - /** - * @deprecated createSync will be replaced with the ability to precompile components from within - * the test. - */ - createSync(componentFactory: ComponentFactory): ComponentFixture { + createSync(rootComponentType: ConcreteType): ComponentFixture { let noNgZone = IS_DART || this._injector.get(ComponentFixtureNoNgZone, false); let ngZone: NgZone = noNgZone ? null : this._injector.get(NgZone, null); + let compiler: Compiler = this._injector.get(Compiler); + + let initComponent = () => { + return this.createFromFactory( + ngZone, this._injector.get(Compiler).compileComponentSync(rootComponentType)); + }; - let initComponent = () => this.createFromFactory(ngZone, componentFactory); return ngZone == null ? initComponent() : ngZone.run(initComponent); } } diff --git a/modules/@angular/facade/src/lang.ts b/modules/@angular/facade/src/lang.ts index f284253c74..d64590407e 100644 --- a/modules/@angular/facade/src/lang.ts +++ b/modules/@angular/facade/src/lang.ts @@ -77,9 +77,7 @@ export interface Type extends Function {} /** * Runtime representation of a type that is constructable (non-abstract). */ -export interface ConcreteType extends Type { new (...args: any[] /** TODO #9100 */): any; } - -export interface ClassWithConstructor { new (...args: any[]): T; } +export interface ConcreteType extends Type { new (...args: any[]): T; } export function getTypeNameForDebugging(type: Type): string { if (type['name']) { diff --git a/tools/public_api_guard/compiler/index.d.ts b/tools/public_api_guard/compiler/index.d.ts index 152e3296fc..03e2984d60 100644 --- a/tools/public_api_guard/compiler/index.d.ts +++ b/tools/public_api_guard/compiler/index.d.ts @@ -310,21 +310,24 @@ export declare class CompilerConfig { }); } -export declare class CompileTemplateMetadata { +export declare class CompileTemplateMetadata implements CompileStylesheetMetadata { animations: CompileAnimationEntryMetadata[]; encapsulation: ViewEncapsulation; + externalStylesheets: CompileStylesheetMetadata[]; interpolation: [string, string]; + moduleUrl: string; ngContentSelectors: string[]; styleUrls: string[]; styles: string[]; template: string; templateUrl: string; - constructor({encapsulation, template, templateUrl, styles, styleUrls, animations, ngContentSelectors, interpolation}?: { + constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations, ngContentSelectors, interpolation}?: { encapsulation?: ViewEncapsulation; template?: string; templateUrl?: string; styles?: string[]; styleUrls?: string[]; + externalStylesheets?: CompileStylesheetMetadata[]; ngContentSelectors?: string[]; animations?: CompileAnimationEntryMetadata[]; interpolation?: [string, string]; @@ -460,9 +463,8 @@ export declare class NormalizedComponentWithViewDirectives { } export declare class OfflineCompiler { - constructor(_directiveNormalizer: DirectiveNormalizer, _templateParser: TemplateParser, _styleCompiler: StyleCompiler, _viewCompiler: ViewCompiler, _outputEmitter: OutputEmitter, _xhr: XHR); - compileTemplates(components: NormalizedComponentWithViewDirectives[]): SourceModule; - loadAndCompileStylesheet(stylesheetUrl: string, shim: boolean, suffix: string): Promise; + constructor(_directiveNormalizer: DirectiveNormalizer, _templateParser: TemplateParser, _styleCompiler: StyleCompiler, _viewCompiler: ViewCompiler, _outputEmitter: OutputEmitter); + compileTemplates(components: NormalizedComponentWithViewDirectives[]): SourceModule[]; normalizeDirectiveMetadata(directive: CompileDirectiveMetadata): Promise; } @@ -515,9 +517,12 @@ export declare abstract class RenderTypes { renderer: CompileIdentifierMetadata; } -export declare class RuntimeCompiler implements ComponentResolver { - constructor(_metadataResolver: CompileMetadataResolver, _templateNormalizer: DirectiveNormalizer, _templateParser: TemplateParser, _styleCompiler: StyleCompiler, _viewCompiler: ViewCompiler, _xhr: XHR, _genConfig: CompilerConfig); +export declare class RuntimeCompiler implements ComponentResolver, Compiler { + constructor(_metadataResolver: CompileMetadataResolver, _templateNormalizer: DirectiveNormalizer, _templateParser: TemplateParser, _styleCompiler: StyleCompiler, _viewCompiler: ViewCompiler, _genConfig: CompilerConfig); clearCache(): void; + clearCacheFor(compType: Type): void; + compileComponentAsync(compType: ConcreteType): Promise>; + compileComponentSync(compType: ConcreteType): ComponentFactory; resolveComponent(component: Type | string): Promise>; } diff --git a/tools/public_api_guard/compiler/testing.d.ts b/tools/public_api_guard/compiler/testing.d.ts index c2cd53e2c4..ad64f46735 100644 --- a/tools/public_api_guard/compiler/testing.d.ts +++ b/tools/public_api_guard/compiler/testing.d.ts @@ -1,4 +1,5 @@ export declare class MockDirectiveResolver extends DirectiveResolver { + constructor(_injector: Injector); resolve(type: Type): DirectiveMetadata; setProvidersOverride(type: Type, providers: any[]): void; setViewProvidersOverride(type: Type, viewProviders: any[]): void; @@ -22,7 +23,7 @@ export declare class MockSchemaRegistry implements ElementSchemaRegistry { } export declare class MockViewResolver extends ViewResolver { - constructor(); + constructor(_injector: Injector); overrideViewDirective(component: Type, from: Type, to: Type): void; resolve(component: Type): ViewMetadata; setAnimations(component: Type, animations: AnimationEntryMetadata[]): void; @@ -32,7 +33,8 @@ export declare class MockViewResolver extends ViewResolver { export declare class OverridingTestComponentBuilder extends TestComponentBuilder { constructor(injector: Injector); - createAsync(rootComponentType: Type): Promise>; + createAsync(rootComponentType: ConcreteType): Promise>; + createSync(rootComponentType: ConcreteType): ComponentFixture; overrideAnimations(componentType: Type, animations: AnimationEntryMetadata[]): TestComponentBuilder; overrideDirective(componentType: Type, from: Type, to: Type): OverridingTestComponentBuilder; overrideProviders(type: Type, providers: any[]): OverridingTestComponentBuilder; diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 1cb4b5817b..c77e8b5255 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -207,7 +207,7 @@ export declare abstract class ChangeDetectorRef { } /** @stable */ -export declare function Class(clsDef: ClassDefinition): ConcreteType; +export declare function Class(clsDef: ClassDefinition): ConcreteType; /** @stable */ export interface ClassDefinition { @@ -226,6 +226,14 @@ export declare class CollectionChangeRecord { toString(): string; } +/** @stable */ +export declare class Compiler { + clearCache(): void; + clearCacheFor(compType: Type): void; + compileComponentAsync(component: ConcreteType): Promise>; + compileComponentSync(component: ConcreteType): ComponentFactory; +} + /** @stable */ export declare var Component: ComponentMetadataFactory; @@ -254,7 +262,7 @@ export declare class ComponentFactory { /** @stable */ export declare abstract class ComponentFactoryResolver { - abstract resolveComponentFactory(component: ClassWithConstructor): ComponentFactory; + abstract resolveComponentFactory(component: ConcreteType): ComponentFactory; static NULL: ComponentFactoryResolver; } @@ -1267,7 +1275,7 @@ export interface TypeDecorator { annotations: any[]; (target: Object, propertyKey?: string | symbol, parameterIndex?: number): void; (type: T): T; - Class(obj: ClassDefinition): ConcreteType; + Class(obj: ClassDefinition): ConcreteType; } /** @stable */ diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts index d025b83aba..3d9fd18415 100644 --- a/tools/public_api_guard/core/testing.d.ts +++ b/tools/public_api_guard/core/testing.d.ts @@ -89,10 +89,10 @@ export declare function setBaseTestProviders(platformProviders: Array>; - createFakeAsync(rootComponentType: Type): ComponentFixture; + createAsync(rootComponentType: ConcreteType): Promise>; + createFakeAsync(rootComponentType: ConcreteType): ComponentFixture; protected createFromFactory(ngZone: NgZone, componentFactory: ComponentFactory): ComponentFixture; - /** @deprecated */ createSync(componentFactory: ComponentFactory): ComponentFixture; + createSync(rootComponentType: ConcreteType): ComponentFixture; overrideAnimations(componentType: Type, animations: AnimationEntryMetadata[]): TestComponentBuilder; overrideDirective(componentType: Type, from: Type, to: Type): TestComponentBuilder; overrideProviders(type: Type, providers: any[]): TestComponentBuilder;