From 86d96122305087626fbb5ae94e38ff9082a2a083 Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Thu, 11 Jan 2018 15:37:56 -0800 Subject: [PATCH] feat(ivy): update compiler to specification (#21657) PR Close #21657 --- packages/compiler/src/aot/compiler.ts | 6 +- packages/compiler/src/constant_pool.ts | 35 +- packages/compiler/src/output/ts_emitter.ts | 12 +- .../compiler/src/render3/r3_identifiers.ts | 6 + .../compiler/src/render3/r3_view_compiler.ts | 236 +++++++++--- .../src/template_parser/template_ast.ts | 4 +- .../src/template_parser/template_parser.ts | 5 +- .../test/render3/r3_view_compiler_spec.ts | 347 +++++++++++++++++- .../template_parser/template_parser_spec.ts | 4 +- 9 files changed, 565 insertions(+), 90 deletions(-) diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 66610497a0..c8f1787c99 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -19,7 +19,7 @@ import {NgModuleCompiler} from '../ng_module_compiler'; import {OutputEmitter} from '../output/abstract_emitter'; import * as o from '../output/output_ast'; import {ParseError} from '../parse_util'; -import {compileComponent as compileIvyComponent} from '../render3/r3_view_compiler'; +import {compileComponent as compileIvyComponent, compileDirective as compileIvyDirective} from '../render3/r3_view_compiler'; import {CompiledStylesheet, StyleCompiler} from '../style_compiler'; import {SummaryResolver} from '../summary_resolver'; import {TemplateAst} from '../template_parser/template_ast'; @@ -325,7 +325,7 @@ export class AotCompiler { const context = this._createOutputContext(fileName); - // Process all components + // Process all components and directives directives.forEach(directiveType => { const directiveMetadata = this._metadataResolver.getDirectiveMetadata(directiveType); if (directiveMetadata.isComponent) { @@ -337,6 +337,8 @@ export class AotCompiler { const {template: parsedTemplate} = this._parseTemplate(directiveMetadata, module, module.transitiveModule.directives); compileIvyComponent(context, directiveMetadata, parsedTemplate, this._reflector); + } else { + compileIvyDirective(context, directiveMetadata, this._reflector); } }); diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index 1284c7d905..06ff1004d1 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -9,6 +9,8 @@ import * as o from './output/output_ast'; import {OutputContext, error} from './util'; +const CONSTANT_PREFIX = '_c'; + export const enum DefinitionKind {Injector, Directive, Component} /** @@ -48,29 +50,34 @@ export class ConstantPool { private literals = new Map(); private injectorDefinitions = new Map(); private directiveDefinitions = new Map(); - private componentDefintions = new Map(); + private componentDefinitions = new Map(); private nextNameIndex = 0; - getConstLiteral(literal: o.Expression): o.Expression { + getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression { const key = this.keyOf(literal); let fixup = this.literals.get(key); + let newValue = false; if (!fixup) { fixup = new FixupExpression(literal); this.literals.set(key, fixup); - } else if (!fixup.shared) { + newValue = true; + } + + if ((!newValue && !fixup.shared) || (newValue && forceShared)) { // Replace the expression with a variable const name = this.freshName(); this.statements.push( o.variable(name).set(literal).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); fixup.fixup(o.variable(name)); } + return fixup; } getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext): o.Expression { const declarations = kind == DefinitionKind.Component ? - this.componentDefintions : + this.componentDefinitions : kind == DefinitionKind.Directive ? this.directiveDefinitions : this.injectorDefinitions; let fixup = declarations.get(type); if (!fixup) { @@ -97,7 +104,7 @@ export class ConstantPool { */ uniqueName(prefix: string): string { return `${prefix}${this.nextNameIndex++}`; } - private freshName(): string { return this.uniqueName(`_$`); } + private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); } private keyOf(expression: o.Expression) { return expression.visitExpression(new KeyVisitor(), null); @@ -105,15 +112,22 @@ export class ConstantPool { } class KeyVisitor implements o.ExpressionVisitor { - visitLiteralExpr(ast: o.LiteralExpr): string { return `${ast.value}`; } + visitLiteralExpr(ast: o.LiteralExpr): string { + return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`; + } visitLiteralArrayExpr(ast: o.LiteralArrayExpr): string { - return ast.entries.map(entry => entry.visitExpression(this, null)).join(','); + return `[${ast.entries.map(entry => entry.visitExpression(this, null)).join(',')}]`; } visitLiteralMapExpr(ast: o.LiteralMapExpr): string { - const entries = - ast.entries.map(entry => `${entry.key}:${entry.value.visitExpression(this, null)}`); - return `{${entries.join(',')}`; + const mapEntry = (entry: o.LiteralMapEntry) => + `${entry.key}:${entry.value.visitExpression(this, null)}`; + return `{${ast.entries.map(mapEntry).join(',')}`; + } + + visitExternalExpr(ast: o.ExternalExpr): string { + return ast.value.moduleName ? `EX:${ast.value.moduleName}:${ast.value.name}` : + `EX:${ast.value.runtime.name}`; } visitReadVarExpr = invalid; @@ -123,7 +137,6 @@ class KeyVisitor implements o.ExpressionVisitor { visitInvokeMethodExpr = invalid; visitInvokeFunctionExpr = invalid; visitInstantiateExpr = invalid; - visitExternalExpr = invalid; visitConditionalExpr = invalid; visitNotExpr = invalid; visitAssertNotNullExpr = invalid; diff --git a/packages/compiler/src/output/ts_emitter.ts b/packages/compiler/src/output/ts_emitter.ts index 58d5921891..60d38b9307 100644 --- a/packages/compiler/src/output/ts_emitter.ts +++ b/packages/compiler/src/output/ts_emitter.ts @@ -40,9 +40,9 @@ export type ReferenceFilter = (reference: o.ExternalReference) => boolean; export class TypeScriptEmitter implements OutputEmitter { emitStatementsAndContext( genFilePath: string, stmts: o.Statement[], preamble: string = '', - emitSourceMaps: boolean = true, - referenceFilter?: ReferenceFilter): {sourceText: string, context: EmitterVisitorContext} { - const converter = new _TsEmitterVisitor(referenceFilter); + emitSourceMaps: boolean = true, referenceFilter?: ReferenceFilter, + importFilter?: ReferenceFilter): {sourceText: string, context: EmitterVisitorContext} { + const converter = new _TsEmitterVisitor(referenceFilter, importFilter); const ctx = EmitterVisitorContext.createRoot(); @@ -83,7 +83,9 @@ export class TypeScriptEmitter implements OutputEmitter { class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor { private typeExpression = 0; - constructor(private referenceFilter?: ReferenceFilter) { super(false); } + constructor(private referenceFilter?: ReferenceFilter, private importFilter?: ReferenceFilter) { + super(false); + } importsWithPrefixes = new Map(); reexports = new Map(); @@ -390,7 +392,7 @@ class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor ctx.print(null, '(null as any)'); return; } - if (moduleName) { + if (moduleName && (!this.importFilter || !this.importFilter(value))) { let prefix = this.importsWithPrefixes.get(moduleName); if (prefix == null) { prefix = `i${this.importsWithPrefixes.size}`; diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 089d9b4283..8c80c69c71 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -40,6 +40,10 @@ export class Identifiers { static containerEnd: o.ExternalReference = {name: 'ɵc', moduleName: CORE}; + static containerRefreshStart: o.ExternalReference = {name: 'ɵcR', moduleName: CORE}; + + static containerRefreshEnd: o.ExternalReference = {name: 'ɵcr', moduleName: CORE}; + static directiveCreate: o.ExternalReference = {name: 'ɵD', moduleName: CORE}; static text: o.ExternalReference = {name: 'ɵT', moduleName: CORE}; @@ -61,6 +65,8 @@ export class Identifiers { static bind9: o.ExternalReference = {name: 'ɵb9', moduleName: CORE}; static bindV: o.ExternalReference = {name: 'ɵbV', moduleName: CORE}; + static memory: o.ExternalReference = {name: 'ɵm', moduleName: CORE}; + static refreshComponent: o.ExternalReference = {name: 'ɵr', moduleName: CORE}; static directiveLifeCycle: o.ExternalReference = {name: 'ɵl', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index a8c5633469..63112d2c0a 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; import {BindingForm, BuiltinConverter, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter'; import {ConstantPool, DefinitionKind} from '../constant_pool'; @@ -21,6 +21,7 @@ import {OutputContext, error} from '../util'; import {Identifiers as R3} from './r3_identifiers'; + /** Name of the context parameter passed into a template function */ const CONTEXT_NAME = 'ctx'; @@ -30,15 +31,40 @@ const CREATION_MODE_FLAG = 'cm'; /** Name of the temporary to use during data binding */ const TEMPORARY_NAME = '_t'; +/** The prefix reference variables */ +const REFERENCE_PREFIX = '_r'; + +export function compileDirective( + outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector) { + const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; + + // e.g. `factory: () => new MyApp(injectElementRef())` + const templateFactory = createFactory(directive.type, outputCtx, reflector); + definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + + const className = identifierName(directive.type) !; + className || error(`Cannot resolver the name of ${directive.type}`); + + // Create the partial class to be merged with the actual class. + outputCtx.statements.push(new o.ClassStmt( + /* name */ className, + /* parent */ null, + /* fields */[new o.ClassField( + /* name */ 'ngDirectiveDef', + /* type */ o.INFERRED_TYPE, + /* modifiers */[o.StmtModifier.Static], + /* initializer */ o.importExpr(R3.defineDirective).callFn([o.literalMap( + definitionMapValues)]))], + /* getters */[], + /* constructorMethod */ new o.ClassMethod(null, [], []), + /* methods */[])); +} + export function compileComponent( outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[], reflector: CompileReflector) { const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = []; - // e.g. `type: MyApp` - definitionMapValues.push( - {key: 'type', value: outputCtx.importExpr(component.type.reference), quoted: false}); - // e.g. `tag: 'my-app' // This is optional and only included if the first selector of a component has element. const selector = component.selector && CssSelector.parse(component.selector); @@ -54,24 +80,27 @@ export function compileComponent( if (selectorAttributes.length) { definitionMapValues.push({ key: 'attrs', - value: outputCtx.constantPool.getConstLiteral(o.literalArr(selectorAttributes.map( - value => value != null ? o.literal(value) : o.literal(undefined)))), + value: outputCtx.constantPool.getConstLiteral( + o.literalArr(selectorAttributes.map( + value => value != null ? o.literal(value) : o.literal(undefined))), + /* forceShared */ true), quoted: false }); } } - // e.g. `template: function(_ctx, _cm) {...}` - const templateFunctionExpression = - new TemplateDefinitionBuilder(outputCtx, outputCtx.constantPool, CONTEXT_NAME) - .buildTemplateFunction(template); - definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); - - // e.g. `factory: () => new MyApp(injectElementRef())` const templateFactory = createFactory(component.type, outputCtx, reflector); definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false}); + // e.g. `template: function(_ctx, _cm) {...}` + const templateFunctionExpression = + new TemplateDefinitionBuilder( + outputCtx, outputCtx.constantPool, CONTEXT_NAME, ROOT_SCOPE.nestedScope()) + .buildTemplateFunction(template); + definitionMapValues.push({key: 'template', value: templateFunctionExpression, quoted: false}); + + const className = identifierName(component.type) !; className || error(`Cannot resolver the name of ${component.type}`); @@ -136,9 +165,49 @@ function interpolate(args: o.Expression[]): o.Expression { return o.importExpr(R3.bindV).callFn(args); } -class TemplateDefinitionBuilder implements TemplateAstVisitor { +class BindingScope { + private map = new Map(); + private referenceNameIndex = 0; + + constructor(private parent: BindingScope|null) {} + + get(name: string): o.Expression|null { + let current: BindingScope|null = this; + while (current) { + const value = current.map.get(name); + if (value != null) { + // Cache the value locally. + this.map.set(name, value); + return value; + } + current = current.parent; + } + return null; + } + + set(name: string, variableName: string): BindingScope { + !this.map.has(name) || + error(`The name ${name} is already defined in scope to be ${this.map.get(name)}`); + this.map.set(name, o.variable(variableName)); + return this; + } + + nestedScope(): BindingScope { return new BindingScope(this); } + + freshReferenceName(): string { + let current: BindingScope|null = this; + // Find the top scope as it maintains the global reference count + while (current.parent) current = current.parent; + return `${REFERENCE_PREFIX}${current.referenceNameIndex++}`; + } +} + +const ROOT_SCOPE = new BindingScope(null).set('$event', '$event'); + +class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { private _dataIndex = 0; private _bindingContext = 0; + private _referenceIndex = 0; private _temporaryAllocated = false; private _prefix: o.Statement[] = []; private _creationMode: o.Statement[] = []; @@ -151,7 +220,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { constructor( private outputCtx: OutputContext, private constantPool: ConstantPool, - private contextParameter: string, private level = 0) {} + private contextParameter: string, private bindingScope: BindingScope, private level = 0) {} buildTemplateFunction(asts: TemplateAst[]): o.FunctionExpr { templateVisitAll(this, asts); @@ -173,7 +242,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { // Host mode (i.e. Comp.h(...)) ...this._hostMode, - // Refesh mode (i.e. Comp.r(...)) + // Refresh mode (i.e. Comp.r(...)) ...this._refreshMode, // Nested templates (i.e. function CompTemplate() {}) @@ -182,31 +251,91 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { o.INFERRED_TYPE); } + getLocal(name: string): o.Expression|null { return this.bindingScope.get(name); } + // TODO(chuckj): Implement ng-content visitNgContent = unknown; + private _computeDirectivesArray(directives: DirectiveAst[]) { + const directiveIndexMap = new Map(); + const directiveExpressions: o.Expression[] = + directives.filter(directive => !directive.directive.isComponent).map(directive => { + directiveIndexMap.set(directive.directive.type.reference, this.allocateDataSlot()); + return this.typeReference(directive.directive.type.reference); + }); + return { + directivesArray: directiveExpressions.length ? + this.constantPool.getConstLiteral( + o.literalArr(directiveExpressions), /* forceShared */ true) : + o.literal(null), + directiveIndexMap + }; + } + visitElement(ast: ElementAst) { let bindingCount = 0; - const elementIndex = this.allocateNode(); + const elementIndex = this.allocateDataSlot(); + let componentIndex: number|undefined = undefined; + const referenceDataSlots = new Map(); // Element creation mode const component = findComponent(ast.directives); + const nullNode = o.literal(null, o.INFERRED_TYPE); const parameters: o.Expression[] = [o.literal(elementIndex)]; + + // Add component type or element tag if (component) { parameters.push(this.typeReference(component.directive.type.reference)); + componentIndex = this.allocateDataSlot(); } else { parameters.push(o.literal(ast.name)); } + // Add attributes array const attributes: o.Expression[] = []; for (let attr of ast.attrs) { attributes.push(o.literal(attr.name), o.literal(attr.value)); } + parameters.push( + attributes.length > 0 ? + this.constantPool.getConstLiteral(o.literalArr(attributes), /* forceShared */ true) : + nullNode); - if (attributes.length !== 0) { - parameters.push(this.constantPool.getConstLiteral(o.literalArr(attributes))); + // Add directives array + const {directivesArray, directiveIndexMap} = this._computeDirectivesArray(ast.directives); + parameters.push(directiveIndexMap.size > 0 ? directivesArray : nullNode); + + if (component && componentIndex != null) { + // Record the data slot for the component + directiveIndexMap.set(component.directive.type.reference, componentIndex); } + // Add references array + if (ast.references && ast.references.length > 0) { + const references = + flatten(ast.references.map(reference => { + const slot = this.allocateDataSlot(); + referenceDataSlots.set(reference.name, slot); + // Generate the update temporary. + const variableName = this.bindingScope.freshReferenceName(); + this._bindingMode.push(o.variable(variableName, o.INFERRED_TYPE) + .set(o.importExpr(R3.memory).callFn([o.literal(slot)])) + .toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final])); + this.bindingScope.set(reference.name, variableName); + return [reference.name, reference.originalValue]; + })).map(value => o.literal(value)); + parameters.push( + this.constantPool.getConstLiteral(o.literalArr(references), /* forceShared */ true)); + } else { + parameters.push(nullNode); + } + + // Remove trailing null nodes as they are implied. + while (parameters[parameters.length - 1] === nullNode) { + parameters.pop(); + } + + // Generate the instruction create element instruction this.instruction(this._creationMode, ast.sourceSpan, R3.createElement, ...parameters); const implicit = o.variable(this.contextParameter); @@ -216,9 +345,9 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { if (input.isAnimation) { this.unsupported('animations'); } - // TODO(chuckj): Builtins transform? + // TODO(chuckj): Built-in transform? const convertedBinding = convertPropertyBinding( - null, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); this._bindingMode.push(...convertedBinding.stmts); const parameters = [o.literal(elementIndex), o.literal(input.name), convertedBinding.currValExpr]; @@ -234,19 +363,20 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { } // Generate directives input bindings - this._visitDirectives(ast.directives, implicit, elementIndex); + this._visitDirectives(ast.directives, implicit, elementIndex, directiveIndexMap); // Traverse element child nodes templateVisitAll(this, ast.children); - // Finish element construction mode. this.instruction(this._creationMode, ast.endSourceSpan || ast.sourceSpan, R3.elementEnd); } - private _visitDirectives(directives: DirectiveAst[], implicit: o.Expression, nodeIndex: number) { + private _visitDirectives( + directives: DirectiveAst[], implicit: o.Expression, nodeIndex: number, + directiveIndexMap: Map) { for (let directive of directives) { - const directiveIndex = this.allocateDirective(); + const directiveIndex = directiveIndexMap.get(directive.directive.type.reference); // Creation mode // e.g. D(0, TodoComponentDef.n(), TodoComponentDef); @@ -258,28 +388,16 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { // node is referenced multiple times to know that it must generate the reference into a // temporary. - this.instruction( - this._creationMode, directive.sourceSpan, R3.directiveCreate, o.literal(directiveIndex), - this.definitionOf(directiveType, kind) - .callMethod(R3.NEW_METHOD, [], directive.sourceSpan), - this.definitionOf(directiveType, kind)); - // Bindings for (const input of directive.inputs) { const convertedBinding = convertPropertyBinding( - null, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this, implicit, input.value, this.bindingContext(), BindingForm.TrySimple, interpolate); this._bindingMode.push(...convertedBinding.stmts); this.instruction( this._bindingMode, directive.sourceSpan, R3.elementProperty, o.literal(input.templateName), o.literal(nodeIndex), convertedBinding.currValExpr); } - // e.g. TodoComponentDef.h(0, 0); - this._hostMode.push( - this.definitionOf(directiveType, kind) - .callMethod(R3.HOST_BINDING_METHOD, [o.literal(directiveIndex), o.literal(nodeIndex)]) - .toStmt()); - // e.g. TodoComponentDef.r(0, 0); this._refreshMode.push( this.definitionOf(directiveType, kind) @@ -289,32 +407,35 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { } visitEmbeddedTemplate(ast: EmbeddedTemplateAst) { - const templateIndex = this.allocateNode(); + const templateIndex = this.allocateDataSlot(); const templateName = `C${templateIndex}Template`; const templateContext = `ctx${this.level}`; - // TODO(chuckj): attrs? + const {directivesArray, directiveIndexMap} = this._computeDirectivesArray(ast.directives); // e.g. C(1, C1Template) this.instruction( this._creationMode, ast.sourceSpan, R3.containerCreate, o.literal(templateIndex), - o.variable(templateName)); + directivesArray, o.variable(templateName)); - // Generate directies + // e.g. Cr(1) + this.instruction( + this._refreshMode, ast.sourceSpan, R3.containerRefreshStart, o.literal(templateIndex)); + + // Generate directives this._visitDirectives( - ast.directives, o.variable(this.contextParameter), - // TODO(chuckj): This should be the element index of the element that contained the template - templateIndex); + ast.directives, o.variable(this.contextParameter), templateIndex, directiveIndexMap); + + // e.g. cr(); + this.instruction(this._refreshMode, ast.sourceSpan, R3.containerRefreshEnd); // Create the template function const templateVisitor = new TemplateDefinitionBuilder( - this.outputCtx, this.constantPool, templateContext, this.level + 1); + this.outputCtx, this.constantPool, templateContext, this.bindingScope.nestedScope(), + this.level + 1); const templateFunctionExpr = templateVisitor.buildTemplateFunction(ast.children); this._postfix.push(templateFunctionExpr.toDeclStmt(templateName, null)); - - // Terminate the definition - this.instruction(this._creationMode, ast.sourceSpan, R3.containerEnd); } // These should be handled in the template or element directly. @@ -325,7 +446,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { readonly visitAttr = invalid; visitBoundText(ast: BoundTextAst) { - const nodeIndex = this.allocateNode(); + const nodeIndex = this.allocateDataSlot(); // Creation mode this.instruction(this._creationMode, ast.sourceSpan, R3.text, o.literal(nodeIndex)); @@ -333,20 +454,21 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { // Refresh mode this.instruction( this._refreshMode, ast.sourceSpan, R3.textCreateBound, o.literal(nodeIndex), - this.bind(o.variable(this.contextParameter), ast.value, ast.sourceSpan)); + this.bind(o.variable(CONTEXT_NAME), ast.value, ast.sourceSpan)); } visitText(ast: TextAst) { // Text is defined in creation mode only. - this.instruction(this._creationMode, ast.sourceSpan, R3.text, o.literal(ast.value)); + this.instruction( + this._creationMode, ast.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), + o.literal(ast.value)); } // These should be handled in the template or element directly readonly visitDirective = invalid; readonly visitDirectiveProperty = invalid; - private allocateDirective() { return this._dataIndex++; } - private allocateNode() { return this._dataIndex++; } + private allocateDataSlot() { return this._dataIndex++; } private bindingContext() { return `${this._bindingContext++}`; } private instruction( @@ -373,13 +495,13 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor { private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression { const convertedPropertyBinding = convertPropertyBinding( - null, implicit, value, this.bindingContext(), BindingForm.TrySimple, interpolate); + this, implicit, value, this.bindingContext(), BindingForm.TrySimple, interpolate); this._refreshMode.push(...convertedPropertyBinding.stmts); return convertedPropertyBinding.currValExpr; } private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression { - return o.importExpr(R3.bind).callFn([this.convertPropertyBinding(implicit, value)]); + return this.convertPropertyBinding(implicit, value); } } diff --git a/packages/compiler/src/template_parser/template_ast.ts b/packages/compiler/src/template_parser/template_ast.ts index 8594d01d67..a4ead18fa0 100644 --- a/packages/compiler/src/template_parser/template_ast.ts +++ b/packages/compiler/src/template_parser/template_ast.ts @@ -110,8 +110,8 @@ export class BoundEventAst implements TemplateAst { */ export class ReferenceAst implements TemplateAst { constructor( - public name: string, public value: CompileTokenMetadata, public sourceSpan: ParseSourceSpan) { - } + public name: string, public value: CompileTokenMetadata, public originalValue: string, + public sourceSpan: ParseSourceSpan) {} visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitReference(this, context); } diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index 47a56f1b3a..9827f0dd22 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -574,7 +574,7 @@ class TemplateParseVisitor implements html.Visitor { if ((elOrDirRef.value.length === 0 && directive.isComponent) || (elOrDirRef.isReferenceToDirective(directive))) { targetReferences.push(new ReferenceAst( - elOrDirRef.name, createTokenForReference(directive.type.reference), + elOrDirRef.name, createTokenForReference(directive.type.reference), elOrDirRef.value, elOrDirRef.sourceSpan)); matchedReferences.add(elOrDirRef.name); } @@ -598,7 +598,8 @@ class TemplateParseVisitor implements html.Visitor { if (isTemplateElement) { refToken = createTokenForExternalReference(this.reflector, Identifiers.TemplateRef); } - targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan)); + targetReferences.push( + new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.value, elOrDirRef.sourceSpan)); } }); return directiveAsts; diff --git a/packages/compiler/test/render3/r3_view_compiler_spec.ts b/packages/compiler/test/render3/r3_view_compiler_spec.ts index 359cca05af..1f1a49cc7b 100644 --- a/packages/compiler/test/render3/r3_view_compiler_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; +import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; import {ViewEncapsulation} from '@angular/core'; import * as ts from 'typescript'; import {ConstantPool} from '../../src/constant_pool'; import * as o from '../../src/output/output_ast'; -import {compileComponent} from '../../src/render3/r3_view_compiler'; +import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler'; import {OutputContext} from '../../src/util'; import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, setup, toMockFileArray} from '../aot/test_util'; @@ -99,8 +99,327 @@ describe('r3_view_compiler', () => { const result = compile(files, angularFiles); expect(result.source).toContain('@angular/core'); }); + + /* These tests are codified version of the tests in compiler_canonical_spec.ts. Every + * test in compiler_canonical_spec.ts should have a corresponding test here. + */ + describe('compiler conformance', () => { + describe('elements', () => { + it('should translate DOM structure', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
Hello World!
\` + }) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + // The factory should look like this: + const factory = 'factory: () => { return new MyComponent(); }'; + + // The template should look like this (where IDENT is a wild card for an identifier): + const template = ` + template: (ctx: IDENT, cm: IDENT) => { + if (cm) { + IDENT.ɵE(0, 'div', IDENT); + IDENT.ɵT(1, 'Hello '); + IDENT.ɵE(2, 'b'); + IDENT.ɵT(3, 'World'); + IDENT.ɵe(); + IDENT.ɵT(4, '!'); + IDENT.ɵe(); + } + } + `; + + // The compiler should also emit a const array like this: + const constants = `const IDENT = ['class', 'my-app', 'title', 'Hello'];`; + + const result = compile(files, angularFiles); + + expectEmit(result.source, factory, 'Incorrect factory'); + expectEmit(result.source, template, 'Incorrect template'); + expectEmit(result.source, constants, 'Incorrect shared constants'); + }); + }); + }); + + describe('components & directives', () => { + it('should instantiate directives', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule} from '@angular/core'; + + @Component({selector: 'child', template: 'child-view'}) + export class ChildComponent {} + + @Directive({selector: '[some-directive]'}) + export class SomeDirective {} + + @Component({selector: 'my-component', template: '!'}) + export class MyComponent {} + + @NgModule({declarations: [ChildComponent, SomeDirective, MyComponent]}) + export class MyModule{} + ` + } + }; + + // ChildComponent definition should be: + const ChildComponentDefinition = ` + static ngComponentDef = IDENT.ɵdefineComponent({ + tag: 'child', + factory: () => { return new ChildComponent(); }, + template: (ctx: IDENT, cm: IDENT) => { + if (cm) { + IDENT.ɵT(0, 'child-view'); + } + } + });`; + + // SomeDirective definition should be: + const SomeDirectiveDefinition = ` + static ngDirectiveDef = IDENT.ɵdefineDirective({ + factory: () => {return new SomeDirective(); } + }); + `; + + // MyComponent definition should be: + const MyComponentDefinition = ` + static ngComponentDef = IDENT.ɵdefineComponent({ + tag: 'my-component', + factory: () => { return new MyComponent(); }, + template: (ctx: IDENT, cm: IDENT) => { + if (cm) { + IDENT.ɵE(0, ChildComponent, IDENT, IDENT); + IDENT.ɵe(); + IDENT.ɵT(3, '!'); + } + ChildComponent.ngComponentDef.r(1, 0); + SomeDirective.ngDirectiveDef.r(2, 0); + } + }); + `; + + // The following constants should be emitted as well. + const AttributesConstant = `const IDENT = ['some-directive', ''];`; + + const DirectivesConstant = `const IDENT = [SomeDirective];`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, ChildComponentDefinition, 'Incorrect ChildComponent.ngComponentDef'); + expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef'); + expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef'); + expectEmit(source, AttributesConstant, 'Incorrect shared attributes constant'); + expectEmit(source, DirectivesConstant, 'Incorrect share directives constant'); + }); + + it('should support structural directives', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, Directive, NgModule, TemplateRef} from '@angular/core'; + + @Directive({selector: '[if]'}) + export class IfDirective { + constructor(template: TemplateRef) { } + } + + @Component({ + selector: 'my-component', + template: '
  • {{salutation}} {{foo}}
' + }) + export class MyComponent { + salutation = 'Hello'; + } + + @NgModule({declarations: [IfDirective, MyComponent]}) + export class MyModule {} + ` + } + }; + + const IfDirectiveDefinition = ` + static ngDirectiveDef = IDENT.ɵdefineDirective({ + factory: () => { return new IfDirective(IDENT.ɵinjectTemplateRef()); } + });`; + const MyComponentDefinition = ` + static ngComponentDef = IDENT.ɵdefineComponent({ + tag: 'my-component', + factory: () => { return new MyComponent(); }, + template: (ctx: IDENT, cm: IDENT) => { + if (cm) { + IDENT.ɵE(0, 'ul', null, null, IDENT); + IDENT.ɵC(2, IDENT, C2Template); + IDENT.ɵe(); + } + const IDENT = IDENT.ɵm(1); + IDENT.ɵcR(2); + IfDirective.ngDirectiveDef.r(3,2); + IDENT.ɵcr(); + + function C2Template(ctx0: IDENT, cm: IDENT) { + if (cm) { + IDENT.ɵE(0, 'li'); + IDENT.ɵT(1); + IDENT.ɵe(); + } + IDENT.ɵt(1, IDENT.ɵb2('', ctx.salutation, ' ', IDENT, '')); + } + } + });`; + const locals = `const IDENT = ['foo', ''];`; + const directives = `const IDENT = [IfDirective];`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, IfDirectiveDefinition, 'Incorrect IfDirective.ngDirectiveDef'); + expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); + expectEmit(source, locals, 'Incorrect share locals constant'); + expectEmit(source, directives, 'Incorrect shared directive constant'); + }); + + it('local reference', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({selector: 'my-component', template: 'Hello {{user.value}}!'}) + export class MyComponent {} + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const MyComponentDefinition = ` + static ngComponentDef = IDENT.ɵdefineComponent({ + tag: 'my-component', + factory: () => { return new MyComponent(); }, + template: (ctx: IDENT, cm: IDENT) => { + if (cm) { + IDENT.ɵE(0, 'input', null, null, IDENT); + IDENT.ɵe(); + IDENT.ɵT(2); + } + const IDENT = IDENT.ɵm(1); + IDENT.ɵt(2, IDENT.ɵb1('Hello ', IDENT.value, '!')); + } + }); + `; + + const locals = ` + const IDENT = ['user', '']; + `; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); + expectEmit(source, locals, 'Incorrect locals constant definition'); + }); + }); }); +const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; +const OPERATOR = + /!|%|\*|\/|\^|\&|\&\&\|\||\|\||\(|\)|\{|\}|\[|\]|:|;|\.|<|<=|>|>=|=|==|===|!=|!==|=>|\+|\+\+|-|--|@|,|\.|\.\.\./; +const STRING = /\'[^'\n]*\'|"[^'\n]*"|`[^`]*`/; +const NUMBER = /[0-9]+/; +const TOKEN = new RegExp( + `^((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source})`); +const WHITESPACE = /^\s+/; + +type Piece = string | RegExp; + +const IDENT = /[A-Za-z$_][A-Za-z0-9$_]*/; + +function tokenize(text: string): Piece[] { + function matches(exp: RegExp): string|false { + const m = text.match(exp); + if (!m) return false; + text = text.substr(m[0].length); + return m[0]; + } + function next(): string { + const result = matches(TOKEN); + if (!result) { + throw Error(`Invalid test, no token found for '${text.substr(0, 30)}...'`); + } + matches(WHITESPACE); + return result; + } + + const pieces: Piece[] = []; + matches(WHITESPACE); + while (text) { + const token = next(); + if (token === 'IDENT') { + pieces.push(IDENT); + } else { + pieces.push(token); + } + } + return pieces; +} + +const contextWidth = 100; +function expectEmit(source: string, emitted: string, description: string) { + const pieces = tokenize(emitted); + const expr = r(...pieces); + if (!expr.test(source)) { + let last: number = 0; + for (let i = 1; i < pieces.length; i++) { + let t = r(...pieces.slice(0, i)); + let m = source.match(t); + let expected = pieces[i - 1] == IDENT ? '' : pieces[i - 1]; + if (!m) { + const contextPieceWidth = contextWidth / 2; + fail( + `${description}: Expected to find ${expected} '${source.substr(0,last)}[<---HERE]${source.substr(last)}'`); + return; + } else { + last = (m.index || 0) + m[0].length; + } + } + fail( + 'Test helper failure: Expected expression failed but the reporting logic could not find where it failed'); + } +} + +const IDENT_LIKE = /^[a-z][A-Z]/; +const SPECIAL_RE_CHAR = /\/|\(|\)|\||\*|\+|\[|\]|\{|\}/g; +function r(...pieces: (string | RegExp)[]): RegExp { + let results: string[] = []; + let first = true; + for (const piece of pieces) { + if (!first) + results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`); + first = false; + if (typeof piece === 'string') { + results.push(piece.replace(SPECIAL_RE_CHAR, s => '\\' + s)); + } else { + results.push('(' + piece.source + ')'); + } + } + return new RegExp(results.join('')); +} + function compile( data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, errorCollector: (error: any, fileName?: string) => void = error => { throw error; }) { @@ -156,7 +475,7 @@ function compile( const directives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys()); - const fakeOuputContext: OutputContext = { + const fakeOutputContext: OutputContext = { genFilePath: 'fakeFactory.ts', statements: [], importExpr(symbol: StaticSymbol, typeParams: o.Type[]) { @@ -182,7 +501,10 @@ function compile( // Compile the directives. for (const directive of directives) { - const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !; + const module = analyzedModules.ngModuleByPipeOrDirective.get(directive); + if (!module || !module.type.reference.filePath.startsWith('/app')) { + continue; + } if (resolver.isDirective(directive)) { const metadata = resolver.getDirectiveMetadata(directive); if (metadata.isComponent) { @@ -196,17 +518,24 @@ function compile( const parsedTemplate = templateParser.parse( metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false); - compileComponent(fakeOuputContext, metadata, parsedTemplate.template, staticReflector); + compileComponent(fakeOutputContext, metadata, parsedTemplate.template, staticReflector); + } else { + compileDirective(fakeOutputContext, metadata, staticReflector); } } } - fakeOuputContext.statements.unshift(...fakeOuputContext.constantPool.statements); + fakeOutputContext.statements.unshift(...fakeOutputContext.constantPool.statements); const emitter = new TypeScriptEmitter(); - const result = emitter.emitStatementsAndContext( - fakeOuputContext.genFilePath, fakeOuputContext.statements, '', false); + const moduleName = compilerHost.fileNameToModuleName( + fakeOutputContext.genFilePath, fakeOutputContext.genFilePath); - return {source: result.sourceText, outputContext: fakeOuputContext}; + const result = emitter.emitStatementsAndContext( + fakeOutputContext.genFilePath, fakeOutputContext.statements, '', false, + /* referenceFilter */ undefined, + /* importFilter */ e => e.moduleName != null && e.moduleName.startsWith('/app')); + + return {source: result.sourceText, outputContext: fakeOutputContext}; } \ No newline at end of file diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index f5943068c1..0cff88a9fd 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -407,7 +407,7 @@ class ArrayConsole implements Console { expectVisitedNode( new class extends NullVisitor{visitReference(ast: ReferenceAst, context: any): any{return ast;}}, - new ReferenceAst('foo', null !, null !)); + new ReferenceAst('foo', null !, null !, null !)); }); it('should visit VariableAst', () => { @@ -474,7 +474,7 @@ class ArrayConsole implements Console { new NgContentAst(0, 0, null !), new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null !), new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null !, null !), - new ReferenceAst('foo', null !, null !), new VariableAst('foo', 'bar', null !), + new ReferenceAst('foo', null !, 'bar', null !), new VariableAst('foo', 'bar', null !), new BoundEventAst('foo', 'bar', 'goo', null !, null !), new BoundElementPropertyAst('foo', null !, null !, null !, 'bar', null !), new AttrAst('foo', 'bar', null !), new BoundTextAst(null !, 0, null !),