diff --git a/packages/compiler-cli/linker/src/file_linker/file_linker.ts b/packages/compiler-cli/linker/src/file_linker/file_linker.ts index ca575604f0..cf19f61bc2 100644 --- a/packages/compiler-cli/linker/src/file_linker/file_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/file_linker.ts @@ -18,7 +18,7 @@ export const NO_STATEMENTS: Readonly = [] as const; * This class is responsible for linking all the partial declarations found in a single file. */ export class FileLinker { - private linkerSelector = new PartialLinkerSelector(); + private linkerSelector = new PartialLinkerSelector(this.linkerEnvironment.options); private emitScopes = new Map>(); constructor( diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index bf0efbfc8e..3a7f4cbdf4 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -5,20 +5,182 @@ * 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 {ConstantPool} from '@angular/compiler'; +import {compileComponentFromMetadata, ConstantPool, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, makeBindingParser, parseTemplate, R3ComponentMetadata, R3UsedDirectiveMetadata} from '@angular/compiler'; +import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/compiler/src/core'; import * as o from '@angular/compiler/src/output/output_ast'; -import {AstObject} from '../../ast/ast_value'; +import {Range} from '../../ast/ast_host'; +import {AstObject, AstValue} from '../../ast/ast_value'; +import {FatalLinkerError} from '../../fatal_linker_error'; +import {LinkerOptions} from '../linker_options'; +import {toR3DirectiveMeta} from './partial_directive_linker_1'; import {PartialLinker} from './partial_linker'; /** * A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions. */ export class PartialComponentLinkerVersion1 implements PartialLinker { + constructor(private readonly options: LinkerOptions) {} + linkPartialDeclaration( sourceUrl: string, code: string, constantPool: ConstantPool, metaObj: AstObject): o.Expression { - throw new Error('Not implemented.'); + const meta = toR3ComponentMeta(metaObj, code, sourceUrl, this.options); + const def = compileComponentFromMetadata(meta, constantPool, makeBindingParser()); + return def.expression; } } + +/** + * This function derives the `R3ComponentMetadata` from the provided AST object. + */ +export function toR3ComponentMeta( + metaObj: AstObject, code: string, sourceUrl: string, + options: LinkerOptions): R3ComponentMetadata { + let interpolation = DEFAULT_INTERPOLATION_CONFIG; + if (metaObj.has('interpolation')) { + interpolation = InterpolationConfig.fromArray( + metaObj.getArray('interpolation').map(entry => entry.getString()) as [string, string]); + } + const templateObj = metaObj.getObject('template'); + const templateSource = templateObj.getValue('source'); + const range = getTemplateRange(templateSource, code); + const isInline = templateObj.getBoolean('isInline'); + + // We always normalize line endings if the template is inline. + const i18nNormalizeLineEndingsInICUs = isInline || options.i18nNormalizeLineEndingsInICUs; + + const template = parseTemplate(code, sourceUrl, { + escapedString: true, + interpolationConfig: interpolation, + range, + enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat, + preserveWhitespaces: + metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false, + i18nNormalizeLineEndingsInICUs, + isInline, + }); + if (template.errors !== null) { + const errors = template.errors.map(err => err.toString()).join('\n'); + throw new FatalLinkerError( + templateSource.expression, `Errors found in the template:\n${errors}`); + } + + let wrapDirectivesAndPipesInClosure = false; + + const directives: R3UsedDirectiveMetadata[] = metaObj.has('directives') ? + metaObj.getArray('directives').map(directive => { + const directiveExpr = directive.getObject(); + const type = directiveExpr.getValue('type'); + const selector = directiveExpr.getString('selector'); + + let typeExpr = type.getOpaque(); + if (type.isFunction()) { + typeExpr = type.getFunctionReturnValue().getOpaque(); + wrapDirectivesAndPipesInClosure = true; + } + return { + type: typeExpr, + selector: selector, + inputs: directiveExpr.has('inputs') ? + directiveExpr.getArray('inputs').map(input => input.getString()) : + [], + outputs: directiveExpr.has('outputs') ? + directiveExpr.getArray('outputs').map(input => input.getString()) : + [], + exportAs: directiveExpr.has('exportAs') ? + directiveExpr.getArray('exportAs').map(exportAs => exportAs.getString()) : + null, + }; + }) : + []; + + const pipes = metaObj.has('pipes') ? metaObj.getObject('pipes').toMap(value => { + if (value.isFunction()) { + wrapDirectivesAndPipesInClosure = true; + return value.getFunctionReturnValue().getOpaque(); + } else { + return value.getOpaque(); + } + }) : + new Map(); + + return { + ...toR3DirectiveMeta(metaObj, code, sourceUrl), + viewProviders: metaObj.has('viewProviders') ? metaObj.getOpaque('viewProviders') : null, + template: { + nodes: template.nodes, + ngContentSelectors: template.ngContentSelectors, + }, + wrapDirectivesAndPipesInClosure, + styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) : [], + encapsulation: metaObj.has('encapsulation') ? + parseEncapsulation(metaObj.getValue('encapsulation')) : + ViewEncapsulation.Emulated, + interpolation, + changeDetection: metaObj.has('changeDetection') ? + parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) : + ChangeDetectionStrategy.Default, + animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null, + relativeContextFilePath: sourceUrl, + i18nUseExternalIds: options.i18nUseExternalIds, + pipes, + directives, + }; +} + +/** + * Determines the `ViewEncapsulation` mode from the AST value's symbol name. + */ +function parseEncapsulation(encapsulation: AstValue): ViewEncapsulation { + const symbolName = encapsulation.getSymbolName(); + if (symbolName === null) { + throw new FatalLinkerError( + encapsulation.expression, 'Expected encapsulation to have a symbol name'); + } + const enumValue = ViewEncapsulation[symbolName as keyof typeof ViewEncapsulation]; + if (enumValue === undefined) { + throw new FatalLinkerError(encapsulation.expression, 'Unsupported encapsulation'); + } + return enumValue; +} + +/** + * Determines the `ChangeDetectionStrategy` from the AST value's symbol name. + */ +function parseChangeDetectionStrategy(changeDetectionStrategy: AstValue): + ChangeDetectionStrategy { + const symbolName = changeDetectionStrategy.getSymbolName(); + if (symbolName === null) { + throw new FatalLinkerError( + changeDetectionStrategy.expression, + 'Expected change detection strategy to have a symbol name'); + } + const enumValue = ChangeDetectionStrategy[symbolName as keyof typeof ChangeDetectionStrategy]; + if (enumValue === undefined) { + throw new FatalLinkerError( + changeDetectionStrategy.expression, 'Unsupported change detection strategy'); + } + return enumValue; +} + +/** + * Update the range to remove the start and end chars, which should be quotes around the template. + */ +function getTemplateRange(templateNode: AstValue, code: string): Range { + const {startPos, endPos, startLine, startCol} = templateNode.getRange(); + + if (!/["'`]/.test(code[startPos]) || code[startPos] !== code[endPos - 1]) { + throw new FatalLinkerError( + templateNode.expression, + `Expected the template string to be wrapped in quotes but got: ${ + code.substring(startPos, endPos)}`); + } + return { + startPos: startPos + 1, + endPos: endPos - 1, + startLine, + startCol: startCol + 1, + }; +} diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts index ca98526274..4002f285d0 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts @@ -5,6 +5,8 @@ * 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 {LinkerOptions} from '../linker_options'; + import {PartialComponentLinkerVersion1} from './partial_component_linker_1'; import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1'; import {PartialLinker} from './partial_linker'; @@ -15,10 +17,12 @@ export class PartialLinkerSelector { 1: new PartialDirectiveLinkerVersion1(), }, 'ɵɵngDeclareComponent': { - 1: new PartialComponentLinkerVersion1(), + 1: new PartialComponentLinkerVersion1(this.options), }, }; + constructor(private options: LinkerOptions) {} + /** * Returns true if there are `PartialLinker` classes that can handle functions with this name. */ diff --git a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts index 46f584b2a2..002f77bf46 100644 --- a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts @@ -6,15 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ +import {LinkerOptions} from '../../..'; import {PartialComponentLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_component_linker_1'; import {PartialDirectiveLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_directive_linker_1'; import {PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector'; describe('PartialLinkerSelector', () => { + const options: LinkerOptions = { + i18nNormalizeLineEndingsInICUs: true, + enableI18nLegacyMessageIdFormat: false, + i18nUseExternalIds: false, + }; + describe('supportsDeclaration()', () => { it('should return true if there is at least one linker that matches the given function name', () => { - const selector = new PartialLinkerSelector(); + const selector = new PartialLinkerSelector(options); expect(selector.supportsDeclaration('ɵɵngDeclareDirective')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclareComponent')).toBe(true); expect(selector.supportsDeclaration('$foo')).toBe(false); @@ -23,7 +30,7 @@ describe('PartialLinkerSelector', () => { describe('getLinker()', () => { it('should return the linker that matches the name and version number', () => { - const selector = new PartialLinkerSelector(); + const selector = new PartialLinkerSelector(options); expect(selector.getLinker('ɵɵngDeclareDirective', 1)) .toBeInstanceOf(PartialDirectiveLinkerVersion1); expect(selector.getLinker('ɵɵngDeclareComponent', 1)) @@ -31,7 +38,7 @@ describe('PartialLinkerSelector', () => { }); it('should throw an error if there is no linker that matches the given name or version', () => { - const selector = new PartialLinkerSelector(); + const selector = new PartialLinkerSelector(options); expect(() => selector.getLinker('$foo', 1)) .toThrowError('Unknown partial declaration function $foo.'); expect(() => selector.getLinker('ɵɵngDeclareDirective', 2)) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 24679125e3..53253cacb0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, SchemaMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler'; +import {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentDef, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; @@ -506,11 +506,15 @@ export class ComponentDecoratorHandler implements const bound = binder.bind({template: metadata.template.nodes}); // The BoundTarget knows which directives and pipes matched the template. - const usedDirectives = bound.getUsedDirectives().map(directive => { + type UsedDirective = R3UsedDirectiveMetadata&{ref: Reference}; + const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => { return { - selector: directive.selector, - expression: this.refEmitter.emit(directive.ref, context), ref: directive.ref, + type: this.refEmitter.emit(directive.ref, context), + selector: directive.selector, + inputs: directive.inputs.propertyNames, + outputs: directive.outputs.propertyNames, + exportAs: directive.exportAs, }; }); @@ -529,15 +533,14 @@ export class ComponentDecoratorHandler implements // Scan through the directives/pipes actually used in the template and check whether any // import which needs to be generated would create a cycle. - const cycleDetected = - usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) || + const cycleDetected = usedDirectives.some(dir => this._isCyclicImport(dir.type, context)) || usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context)); if (!cycleDetected) { // No cycle was detected. Record the imports that need to be created in the cycle detector // so that future cyclic import checks consider their production. - for (const {expression} of usedDirectives) { - this._recordSyntheticImport(expression, context); + for (const {type} of usedDirectives) { + this._recordSyntheticImport(type, context); } for (const {expression} of usedPipes) { this._recordSyntheticImport(expression, context); @@ -548,7 +551,7 @@ export class ComponentDecoratorHandler implements // declared after this component. const wrapDirectivesAndPipesInClosure = usedDirectives.some( - dir => isExpressionForwardReference(dir.expression, node.name, context)) || + dir => isExpressionForwardReference(dir.type, node.name, context)) || usedPipes.some( pipe => isExpressionForwardReference(pipe.expression, node.name, context)); @@ -599,18 +602,35 @@ export class ComponentDecoratorHandler implements node: ClassDeclaration, analysis: Readonly, resolution: Readonly, pool: ConstantPool): CompileResult[] { const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; - const res = compileComponentFromMetadata(meta, pool, makeBindingParser()); - const factoryRes = compileNgFactoryDefField( - {...meta, injectFn: Identifiers.directiveInject, target: R3FactoryTarget.Component}); + const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); + return this.compileComponent(analysis, def); + } + + compilePartial( + node: ClassDeclaration, analysis: Readonly, + resolution: Readonly): CompileResult[] { + const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; + const def = compileDeclareComponentFromMetadata(meta, analysis.template); + return this.compileComponent(analysis, def); + } + + private compileComponent( + analysis: Readonly, + {expression: initializer, type}: R3ComponentDef): CompileResult[] { + const factoryRes = compileNgFactoryDefField({ + ...analysis.meta, + injectFn: Identifiers.directiveInject, + target: R3FactoryTarget.Component, + }); if (analysis.metadataStmt !== null) { factoryRes.statements.push(analysis.metadataStmt); } return [ factoryRes, { name: 'ɵcmp', - initializer: res.expression, + initializer, statements: [], - type: res.type, + type, } ]; } @@ -737,7 +757,8 @@ export class ComponentDecoratorHandler implements } const template = this._parseTemplate( - component, templateStr, sourceMapUrl(resourceUrl), /* templateRange */ undefined, + component, templateStr, /* templateLiteral */ null, sourceMapUrl(resourceUrl), + /* templateRange */ undefined, /* escapedString */ false); return { @@ -763,6 +784,7 @@ export class ComponentDecoratorHandler implements const templateExpr = component.get('template')!; let templateStr: string; + let templateLiteral: ts.Node|null = null; let templateUrl: string = ''; let templateRange: LexerRange|undefined = undefined; let sourceMapping: TemplateSourceMapping; @@ -774,6 +796,7 @@ export class ComponentDecoratorHandler implements // strip templateRange = getTemplateRange(templateExpr); templateStr = templateExpr.getSourceFile().text; + templateLiteral = templateExpr; templateUrl = containingFile; escapedString = true; sourceMapping = { @@ -795,15 +818,16 @@ export class ComponentDecoratorHandler implements }; } - const template = - this._parseTemplate(component, templateStr, templateUrl, templateRange, escapedString); + const template = this._parseTemplate( + component, templateStr, templateLiteral, templateUrl, templateRange, escapedString); return {...template, sourceMapping}; } private _parseTemplate( - component: Map, templateStr: string, templateUrl: string, - templateRange: LexerRange|undefined, escapedString: boolean): ParsedComponentTemplate { + component: Map, templateStr: string, templateLiteral: ts.Node|null, + templateUrl: string, templateRange: LexerRange|undefined, + escapedString: boolean): ParsedComponentTemplate { let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces; if (component.has('preserveWhitespaces')) { const expr = component.get('preserveWhitespaces')!; @@ -829,6 +853,7 @@ export class ComponentDecoratorHandler implements // We always normalize line endings if the template has been escaped (i.e. is inline). const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs; + const isInline = component.has('template'); const parsedTemplate = parseTemplate(templateStr, templateUrl, { preserveWhitespaces, interpolationConfig, @@ -836,6 +861,7 @@ export class ComponentDecoratorHandler implements escapedString, enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, i18nNormalizeLineEndingsInICUs, + isInline, }); // Unfortunately, the primary parse of the template above may not contain accurate source map @@ -859,14 +885,15 @@ export class ComponentDecoratorHandler implements enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, i18nNormalizeLineEndingsInICUs, leadingTriviaChars: [], + isInline, }); return { ...parsedTemplate, diagNodes, - template: templateStr, + template: templateLiteral !== null ? new WrappedNodeExpr(templateLiteral) : templateStr, templateUrl, - isInline: component.has('template'), + isInline, file: new ParseSourceFile(templateStr, templateUrl), }; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/GOLDEN_PARTIAL.js index 538f572463..dfa610334a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/elements/GOLDEN_PARTIAL.js @@ -6,18 +6,7 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 5, vars: 0, consts: [["title", "Hello", 1, "my-app"], ["cx", "20", "cy", "30", "r", "50"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelementStart(0, "div", 0); - i0.ɵɵnamespaceSVG(); - i0.ɵɵelementStart(1, "svg"); - i0.ɵɵelement(2, "circle", 1); - i0.ɵɵelementEnd(); - i0.ɵɵnamespaceHTML(); - i0.ɵɵelementStart(3, "p"); - i0.ɵɵtext(4, "test"); - i0.ɵɵelementEnd(); - i0.ɵɵelementEnd(); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '

test

', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -56,18 +45,7 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 5, vars: 0, consts: [["title", "Hello", 1, "my-app"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelementStart(0, "div", 0); - i0.ɵɵnamespaceMathML(); - i0.ɵɵelementStart(1, "math"); - i0.ɵɵelement(2, "infinity"); - i0.ɵɵelementEnd(); - i0.ɵɵnamespaceHTML(); - i0.ɵɵelementStart(3, "p"); - i0.ɵɵtext(4, "test"); - i0.ɵɵelementEnd(); - i0.ɵɵelementEnd(); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '

test

', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -106,15 +84,7 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 5, vars: 0, consts: [["title", "Hello", 1, "my-app"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelementStart(0, "div", 0); - i0.ɵɵtext(1, "Hello "); - i0.ɵɵelementStart(2, "b"); - i0.ɵɵtext(3, "World"); - i0.ɵɵelementEnd(); - i0.ɵɵtext(4, "!"); - i0.ɵɵelementEnd(); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '
Hello World!
', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -153,15 +123,7 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 5, vars: 0, consts: [[0, "xmlns", "foo", "http://someuri/foo", 0, "foo", "bar", "baz", "title", "Hello", 0, "foo", "qux", "quacks", 1, "my-app"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelementStart(0, "div", 0); - i0.ɵɵtext(1, "Hello "); - i0.ɵɵelementStart(2, "b"); - i0.ɵɵtext(3, "World"); - i0.ɵɵelementEnd(); - i0.ɵɵtext(4, "!"); - i0.ɵɵelementEnd(); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '
Hello World!
', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -200,14 +162,7 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 4, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelementContainerStart(0); - i0.ɵɵelementStart(1, "span"); - i0.ɵɵtext(2, "in a "); - i0.ɵɵelementEnd(); - i0.ɵɵtext(3, "container"); - i0.ɵɵelementContainerEnd(); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: 'in a container', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -246,9 +201,7 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelementContainer(0); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ selector: 'my-component', template: '' }] @@ -287,11 +240,7 @@ export class MyComponent { } } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 1, vars: 1, consts: [[3, "id"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelement(0, "div", 0); - } if (rf & 2) { - i0.ɵɵproperty("id", ctx.id); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '
', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ selector: 'my-component', template: '
' }] @@ -325,20 +274,18 @@ export declare class MyModule { ****************************************************************************************************/ import { Component, NgModule } from '@angular/core'; import * as i0 from "@angular/core"; -const _c0 = function (a0) { return [a0]; }; -const _c1 = function () { return [0]; }; export class MyComponent { constructor() { this.id = 'one'; } } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 2, vars: 15, consts: [[3, "ternary", "pipe", "and", "or"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelement(0, "div", 0); - i0.ɵɵpipe(1, "pipe"); - } if (rf & 2) { - i0.ɵɵproperty("ternary", ctx.cond ? i0.ɵɵpureFunction1(8, _c0, ctx.a) : i0.ɵɵpureFunction0(10, _c1))("pipe", i0.ɵɵpipeBind3(1, 4, ctx.value, 1, 2))("and", ctx.cond && i0.ɵɵpureFunction1(11, _c0, ctx.b))("or", ctx.cond || i0.ɵɵpureFunction1(13, _c0, ctx.c)); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: `
`, isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -380,20 +327,13 @@ export declare class MyModule { ****************************************************************************************************/ import { Component, Input, NgModule } from '@angular/core'; import * as i0 from "@angular/core"; -const _c0 = function (a0, a1) { return { collapsedHeight: a0, expandedHeight: a1 }; }; -const _c1 = function (a0, a1) { return { value: a0, params: a1 }; }; -const _c2 = function (a0, a1) { return { collapsedWidth: a0, expandedWidth: a1 }; }; export class MyComponent { getExpandedState() { return 'expanded'; } } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], hostVars: 14, hostBindings: function MyComponent_HostBindings(rf, ctx) { if (rf & 2) { - i0.ɵɵsyntheticHostProperty("@expansionHeight", i0.ɵɵpureFunction2(5, _c1, ctx.getExpandedState(), i0.ɵɵpureFunction2(2, _c0, ctx.collapsedHeight, ctx.expandedHeight)))("@expansionWidth", i0.ɵɵpureFunction2(11, _c1, ctx.getExpandedState(), i0.ɵɵpureFunction2(8, _c2, ctx.collapsedWidth, ctx.expandedWidth))); - } }, inputs: { expandedHeight: "expandedHeight", collapsedHeight: "collapsedHeight", expandedWidth: "expandedWidth", collapsedWidth: "collapsedWidth" }, decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵtext(0, "..."); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", inputs: { expandedHeight: "expandedHeight", collapsedHeight: "collapsedHeight", expandedWidth: "expandedWidth", collapsedWidth: "collapsedWidth" }, host: { properties: { "@expansionHeight": "{\n value: getExpandedState(),\n params: {\n collapsedHeight: collapsedHeight,\n expandedHeight: expandedHeight\n }\n }", "@expansionWidth": "{\n value: getExpandedState(),\n params: {\n collapsedWidth: collapsedWidth,\n expandedWidth: expandedWidth\n }\n }" } }, ngImport: i0, template: { source: '...', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -465,12 +405,7 @@ export class MyComponent { } } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 1, vars: 4, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelement(0, "div"); - } if (rf & 2) { - i0.ɵɵstyleProp("background-color", ctx.color); - i0.ɵɵclassProp("error", ctx.error); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '
', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ @@ -511,10 +446,10 @@ import * as i0 from "@angular/core"; export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 2, vars: 0, consts: [["title", "hi"]], template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelement(0, "div", 0); - i0.ɵɵelement(1, "span", 0); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: ` +
+ + `, isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/interpolations/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/interpolations/GOLDEN_PARTIAL.js index 2f592047b2..16282198a6 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/interpolations/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/interpolations/GOLDEN_PARTIAL.js @@ -9,11 +9,7 @@ export class MyApp { } } MyApp.ɵfac = function MyApp_Factory(t) { return new (t || MyApp)(); }; -MyApp.ɵcmp = i0.ɵɵdefineComponent({ type: MyApp, selectors: [["my-app"]], decls: 1, vars: 9, template: function MyApp_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵtext(0); - } if (rf & 2) { - i0.ɵɵtextInterpolateV([" ", ctx.list[0], " ", ctx.list[1], " ", ctx.list[2], " ", ctx.list[3], " ", ctx.list[4], " ", ctx.list[5], " ", ctx.list[6], " ", ctx.list[7], " ", ctx.list[8], " "]); - } }, encapsulation: 2 }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyApp, selector: "my-app", ngImport: i0, template: { source: ' {{list[0]}} {{list[1]}} {{list[2]}} {{list[3]}} {{list[4]}} {{list[5]}} {{list[6]}} {{list[7]}} {{list[8]}} ', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyApp, [{ type: Component, args: [{ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_directives/directives/matching/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_directives/directives/matching/GOLDEN_PARTIAL.js index e3ce742731..1821e346a0 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_directives/directives/matching/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_directives/directives/matching/GOLDEN_PARTIAL.js @@ -14,9 +14,7 @@ I18nDirective.ɵdir = i0.ɵɵngDeclareDirective({ version: 1, type: I18nDirectiv export class MyComponent { } MyComponent.ɵfac = function MyComponent_Factory(t) { return new (t || MyComponent)(); }; -MyComponent.ɵcmp = i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵelement(0, "div"); - } }, encapsulation: 2 }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ version: 1, type: MyComponent, selector: "my-component", ngImport: i0, template: { source: '
', isInline: true } }); /*@__PURE__*/ (function () { i0.ɵsetClassMetadata(MyComponent, [{ type: Component, args: [{ selector: 'my-component', template: '
' }] diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 84d6e2d30c..a1041f372e 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -102,6 +102,7 @@ export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compile export {makeBindingParser, ParsedTemplate, parseTemplate, ParseTemplateOptions} from './render3/view/template'; export {R3Reference} from './render3/util'; export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler'; +export {compileDeclareComponentFromMetadata} from './render3/partial/component'; export {compileDeclareDirectiveFromMetadata} from './render3/partial/directive'; export {publishFacade} from './jit_compiler_facade'; // This file only reexports content of the `src` folder. Keep it that way. diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index df6bf9966b..d60b8be014 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -154,7 +154,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { preserveWhitespaces: boolean; animations: any[]|undefined; pipes: Map; - directives: {selector: string, expression: any}[]; + directives: R3UsedDirectiveMetadata[]; styles: string[]; encapsulation: ViewEncapsulation; viewProviders: Provider[]|null; @@ -162,6 +162,14 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { changeDetection?: ChangeDetectionStrategy; } +export interface R3UsedDirectiveMetadata { + selector: string; + inputs: string[]; + outputs: string[]; + exportAs: string[]|null; + type: any; +} + export interface R3FactoryDefMetadataFacade { name: string; type: any; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 540c0a3e22..e7a7ab4342 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -21,7 +21,7 @@ import {R3JitReflector} from './render3/r3_jit'; import {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler'; import {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler'; import {R3Reference} from './render3/util'; -import {R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api'; +import {R3ComponentMetadata, R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api'; import {compileComponentFromMetadata, compileDirectiveFromMetadata, ParsedHostBindings, parseHostBindings, verifyHostBindings} from './render3/view/compiler'; import {makeBindingParser, parseTemplate} from './render3/view/template'; import {ResourceLoader} from './resource_loader'; @@ -136,7 +136,7 @@ export class CompilerFacadeImpl implements CompilerFacade { // Compile the component metadata, including template, into an expression. // TODO(alxhub): implement inputs, outputs, queries, etc. - const metadata = { + const metadata: R3ComponentMetadata = { ...facade as R3ComponentMetadataFacadeNoPropAndWhitespace, ...convertDirectiveFacadeToMetadata(facade), selector: facade.selector || this.elementSchemaRegistry.getDefaultComponentElementName(), diff --git a/packages/compiler/src/render3/partial/api.ts b/packages/compiler/src/render3/partial/api.ts index b02a3a99c5..bb4abd04e7 100644 --- a/packages/compiler/src/render3/partial/api.ts +++ b/packages/compiler/src/render3/partial/api.ts @@ -5,6 +5,8 @@ * 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 {ChangeDetectionStrategy, ViewEncapsulation} from '../../core'; +import {InterpolationConfig} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; /** @@ -112,6 +114,106 @@ export interface R3DeclareDirectiveMetadata { ngImport: o.Expression; } +/** + * An extension of `R3DeclareDirectiveMetadata` that declares the shape of a partial declaration of + * a component. + */ +export interface R3DeclareComponentMetadata extends R3DeclareDirectiveMetadata { + /** + * Information about the component's template. + */ + template: { + /** + * The component's unparsed template string as opaque expression. The template is represented + * using either a string literal or template literal without substitutions, but its value is + * not read directly. Instead, the template parser is given the full source file's text and + * the range of this expression to parse directly from source. + */ + source: o.Expression; + + /** + * Whether the template was inline (using `template`) or external (using `templateUrl`). + */ + isInline: boolean; + }; + + /** + * CSS from inline styles and included styleUrls. + */ + styles?: string[]; + + /** + * List of directives which matched in the template, including sufficient + * metadata for each directive to attribute bindings and references within + * the template to each directive specifically, if the runtime instructions + * support this. + */ + directives?: { + /** + * Selector of the directive. + */ + selector: string; + + /** + * Reference to the directive class (possibly a forward reference). + */ + type: o.Expression | (() => o.Expression); + + /** + * Property names of the directive's inputs. + */ + inputs?: string[]; + + /** + * Event names of the directive's outputs. + */ + outputs?: string[]; + + /** + * Names by which this directive exports itself for references. + */ + exportAs?: string[]; + }[]; + + /** + * A map of pipe names to an expression referencing the pipe type (possibly a forward reference) + * which are used in the template. + */ + pipes?: {[pipeName: string]: o.Expression|(() => o.Expression)}; + + /** + * The list of view providers defined in the component. + */ + viewProviders?: o.Expression; + + /** + * A collection of animation triggers that will be used in the component template. + */ + animations?: o.Expression; + + /** + * Strategy used for detecting changes in the component. + * Defaults to `ChangeDetectionStrategy.Default`. + */ + changeDetection?: ChangeDetectionStrategy; + + /** + * An encapsulation policy for the template and CSS styles. + * Defaults to `ViewEncapsulation.Emulated`. + */ + encapsulation?: ViewEncapsulation; + + /** + * Overrides the default interpolation start and end delimiters. Defaults to {{ and }}. + */ + interpolation?: InterpolationConfig; + + /** + * Whether whitespace in the template should be preserved. Defaults to false. + */ + preserveWhitespaces?: boolean; +} + export interface R3DeclareQueryMetadata { /** * Name of the property on the class to update with query results. diff --git a/packages/compiler/src/render3/partial/component.ts b/packages/compiler/src/render3/partial/component.ts new file mode 100644 index 0000000000..050207051b --- /dev/null +++ b/packages/compiler/src/render3/partial/component.ts @@ -0,0 +1,126 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as core from '../../core'; +import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; +import * as o from '../../output/output_ast'; +import {Identifiers as R3} from '../r3_identifiers'; +import {R3ComponentDef, R3ComponentMetadata} from '../view/api'; +import {createComponentType} from '../view/compiler'; +import {ParsedTemplate} from '../view/template'; +import {DefinitionMap} from '../view/util'; + +import {createDirectiveDefinitionMap} from './directive'; +import {toOptionalLiteralArray} from './util'; + + +/** + * Compile a component declaration defined by the `R3ComponentMetadata`. + */ +export function compileDeclareComponentFromMetadata( + meta: R3ComponentMetadata, template: ParsedTemplate): R3ComponentDef { + const definitionMap = createComponentDefinitionMap(meta, template); + + const expression = o.importExpr(R3.declareComponent).callFn([definitionMap.toLiteralMap()]); + const type = createComponentType(meta); + + return {expression, type}; +} + +/** + * Gathers the declaration fields for a component into a `DefinitionMap`. + */ +export function createComponentDefinitionMap( + meta: R3ComponentMetadata, template: ParsedTemplate): DefinitionMap { + const definitionMap = createDirectiveDefinitionMap(meta); + + const templateMap = compileTemplateDefinition(template); + + definitionMap.set('template', templateMap); + + definitionMap.set('styles', toOptionalLiteralArray(meta.styles, o.literal)); + definitionMap.set('directives', compileUsedDirectiveMetadata(meta)); + definitionMap.set('pipes', compileUsedPipeMetadata(meta)); + definitionMap.set('viewProviders', meta.viewProviders); + definitionMap.set('animations', meta.animations); + + if (meta.changeDetection !== undefined) { + definitionMap.set( + 'changeDetection', + o.importExpr(R3.ChangeDetectionStrategy) + .prop(core.ChangeDetectionStrategy[meta.changeDetection])); + } + if (meta.encapsulation !== core.ViewEncapsulation.Emulated) { + definitionMap.set( + 'encapsulation', + o.importExpr(R3.ViewEncapsulation).prop(core.ViewEncapsulation[meta.encapsulation])); + } + if (meta.interpolation !== DEFAULT_INTERPOLATION_CONFIG) { + definitionMap.set( + 'interpolation', + o.literalArr([o.literal(meta.interpolation.start), o.literal(meta.interpolation.end)])); + } + + if (template.preserveWhitespaces === true) { + definitionMap.set('preserveWhitespaces', o.literal(true)); + } + + return definitionMap; +} + +/** + * Compiles the provided template into its partial definition. + */ +function compileTemplateDefinition(template: ParsedTemplate): o.LiteralMapExpr { + const templateMap = new DefinitionMap(); + const templateExpr = + typeof template.template === 'string' ? o.literal(template.template) : template.template; + templateMap.set('source', templateExpr); + templateMap.set('isInline', o.literal(template.isInline)); + return templateMap.toLiteralMap(); +} + +/** + * Compiles the directives as registered in the component metadata into an array literal of the + * individual directives. If the component does not use any directives, then null is returned. + */ +function compileUsedDirectiveMetadata(meta: R3ComponentMetadata): o.LiteralArrayExpr|null { + const wrapType = meta.wrapDirectivesAndPipesInClosure ? + (expr: o.Expression) => o.fn([], [new o.ReturnStatement(expr)]) : + (expr: o.Expression) => expr; + + return toOptionalLiteralArray(meta.directives, directive => { + const dirMeta = new DefinitionMap(); + dirMeta.set('type', wrapType(directive.type)); + dirMeta.set('selector', o.literal(directive.selector)); + dirMeta.set('inputs', toOptionalLiteralArray(directive.inputs, o.literal)); + dirMeta.set('outputs', toOptionalLiteralArray(directive.outputs, o.literal)); + dirMeta.set('exportAs', toOptionalLiteralArray(directive.exportAs, o.literal)); + return dirMeta.toLiteralMap(); + }); +} + +/** + * Compiles the pipes as registered in the component metadata into an object literal, where the + * pipe's name is used as key and a reference to its type as value. If the component does not use + * any pipes, then null is returned. + */ +function compileUsedPipeMetadata(meta: R3ComponentMetadata): o.LiteralMapExpr|null { + if (meta.pipes.size === 0) { + return null; + } + + const wrapType = meta.wrapDirectivesAndPipesInClosure ? + (expr: o.Expression) => o.fn([], [new o.ReturnStatement(expr)]) : + (expr: o.Expression) => expr; + + const entries = []; + for (const [name, pipe] of meta.pipes) { + entries.push({key: name, value: wrapType(pipe), quoted: true}); + } + return o.literalMap(entries); +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 22daaeb91c..7f757c13dd 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -234,9 +234,19 @@ export class Identifiers { static resolveBody: o.ExternalReference = {name: 'ɵɵresolveBody', moduleName: CORE}; static defineComponent: o.ExternalReference = {name: 'ɵɵdefineComponent', moduleName: CORE}; + static declareComponent: o.ExternalReference = {name: 'ɵɵngDeclareComponent', moduleName: CORE}; static setComponentScope: o.ExternalReference = {name: 'ɵɵsetComponentScope', moduleName: CORE}; + static ChangeDetectionStrategy: o.ExternalReference = { + name: 'ChangeDetectionStrategy', + moduleName: CORE, + }; + static ViewEncapsulation: o.ExternalReference = { + name: 'ViewEncapsulation', + moduleName: CORE, + }; + static ComponentDefWithMeta: o.ExternalReference = { name: 'ɵɵComponentDefWithMeta', moduleName: CORE, diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 8e92fe39a9..51f1b310ef 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -149,7 +149,7 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { * A list of directive selectors and an expression referencing the directive type which are in the * scope of the compilation. */ - directives: {selector: string, expression: o.Expression}[]; + directives: R3UsedDirectiveMetadata[]; /** * Whether to wrap the 'directives' and/or `pipes` array, if one is generated, in a closure. @@ -206,6 +206,37 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { changeDetection?: ChangeDetectionStrategy; } +/** + * Information about a directive that is used in a component template. Only the stable, public + * facing information of the directive is stored here. + */ +export interface R3UsedDirectiveMetadata { + /** + * The type of the directive as an expression. + */ + type: o.Expression; + + /** + * The selector of the directive. + */ + selector: string; + + /** + * The binding property names of the inputs of the directive. + */ + inputs: string[]; + + /** + * The binding property names of the outputs of the directive. + */ + outputs: string[]; + + /** + * Name under which the directive is exported, if any (exportAs in Angular). Null otherwise. + */ + exportAs: string[]|null; +} + /** * Information needed to compile a query (view or content). */ diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index c0a4ced776..7bb1788593 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -159,8 +159,8 @@ export function compileComponentFromMetadata( if (meta.directives.length > 0) { const matcher = new SelectorMatcher(); - for (const {selector, expression} of meta.directives) { - matcher.addSelectables(CssSelector.parse(selector), expression); + for (const {selector, type} of meta.directives) { + matcher.addSelectables(CssSelector.parse(selector), type); } directiveMatcher = matcher; } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index a8deb2dab2..4a96cf91ea 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -2032,6 +2032,11 @@ export interface ParseTemplateOptions { * The default is `false`, but this will be switched in a future major release. */ i18nNormalizeLineEndingsInICUs?: boolean; + + /** + * Whether the template was inline. + */ + isInline?: boolean; } /** @@ -2044,6 +2049,7 @@ export interface ParseTemplateOptions { export function parseTemplate( template: string, templateUrl: string, options: ParseTemplateOptions = {}): ParsedTemplate { const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options; + const isInline = options.isInline ?? false; const bindingParser = makeBindingParser(interpolationConfig); const htmlParser = new HtmlParser(); const parseResult = htmlParser.parse( @@ -2057,6 +2063,7 @@ export function parseTemplate( interpolationConfig, preserveWhitespaces, template, + isInline, errors: parseResult.errors, nodes: [], styleUrls: [], @@ -2081,6 +2088,7 @@ export function parseTemplate( interpolationConfig, preserveWhitespaces, template, + isInline, errors: i18nMetaResult.errors, nodes: [], styleUrls: [], @@ -2112,6 +2120,7 @@ export function parseTemplate( preserveWhitespaces, errors: errors.length > 0 ? errors : null, template, + isInline, nodes, styleUrls, styles, @@ -2269,12 +2278,18 @@ export interface ParsedTemplate { interpolationConfig?: InterpolationConfig; /** - * The string contents of the template. + * The string contents of the template, or an expression that represents the string/template + * literal as it occurs in the source. * * This is the "logical" template string, after expansion of any escaped characters (for inline * templates). This may differ from the actual template bytes as they appear in the .ts file. */ - template: string; + template: string|o.Expression; + + /** + * Whether the template was inline (using `template`) or external (using `templateUrl`). + */ + isInline: boolean; /** * Any errors from parsing the template the first time. diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index df6bf9966b..d60b8be014 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -154,7 +154,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { preserveWhitespaces: boolean; animations: any[]|undefined; pipes: Map; - directives: {selector: string, expression: any}[]; + directives: R3UsedDirectiveMetadata[]; styles: string[]; encapsulation: ViewEncapsulation; viewProviders: Provider[]|null; @@ -162,6 +162,14 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { changeDetection?: ChangeDetectionStrategy; } +export interface R3UsedDirectiveMetadata { + selector: string; + inputs: string[]; + outputs: string[]; + exportAs: string[]|null; + type: any; +} + export interface R3FactoryDefMetadataFacade { name: string; type: any; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 30ccf1bb7e..e87609da44 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -165,6 +165,7 @@ export { ɵɵnamespaceMathML, ɵɵnamespaceSVG, ɵɵnextContext, + ɵɵngDeclareComponent, ɵɵngDeclareDirective, ɵɵNgOnChangesFeature, ɵɵpipe, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 2552c2e887..483f45e7b4 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -135,6 +135,7 @@ export { } from './interfaces/node'; export {CssSelectorList, ProjectionSlots} from './interfaces/projection'; export { + ɵɵngDeclareComponent, ɵɵngDeclareDirective, } from './jit/partial'; export { diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index fced9d7c56..be4aae16ad 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -10,8 +10,6 @@ import {ɵɵinject, ɵɵinvalidFactoryDep} from '../../di/injector_compatibility import {ɵɵdefineInjectable, ɵɵdefineInjector} from '../../di/interface/defs'; import * as sanitization from '../../sanitization/sanitization'; import * as r3 from '../index'; -import * as partial from './partial'; - /** @@ -169,6 +167,4 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl, 'ɵɵtrustConstantHtml': sanitization.ɵɵtrustConstantHtml, 'ɵɵtrustConstantResourceUrl': sanitization.ɵɵtrustConstantResourceUrl, - - 'ɵɵngDeclareDirective': partial.ɵɵngDeclareDirective, }))(); diff --git a/packages/core/src/render3/jit/partial.ts b/packages/core/src/render3/jit/partial.ts index 133fe0c62b..402fcd499b 100644 --- a/packages/core/src/render3/jit/partial.ts +++ b/packages/core/src/render3/jit/partial.ts @@ -14,3 +14,12 @@ export function ɵɵngDeclareDirective(decl: unknown): unknown { throw new Error('Not yet implemented'); } + +/** + * Compiles a partial component declaration object into a full component definition object. + * + * @codeGenApi + */ +export function ɵɵngDeclareComponent(decl: unknown): unknown { + throw new Error('Not yet implemented'); +} diff --git a/packages/core/test/render3/jit_environment_spec.ts b/packages/core/test/render3/jit_environment_spec.ts index fa2b19dbcd..4f09a0910b 100644 --- a/packages/core/test/render3/jit_environment_spec.ts +++ b/packages/core/test/render3/jit_environment_spec.ts @@ -21,6 +21,17 @@ const INTERFACE_EXCEPTIONS = new Set([ 'ModuleWithProviders', ]); +/** + * The following symbols are only referenced from partial declaration compilation outputs, which + * will never be emitted by the JIT compiler so are allowed to be omitted from the JIT environment. + */ +const PARTIAL_ONLY = new Set([ + 'ɵɵngDeclareDirective', + 'ɵɵngDeclareComponent', + 'ChangeDetectionStrategy', + 'ViewEncapsulation', +]); + describe('r3 jit environment', () => { // This test keeps render3/jit/environment and r3_identifiers in the compiler in sync, ensuring // that if the compiler writes a reference to a render3 symbol, it will be resolvable at runtime @@ -33,7 +44,7 @@ describe('r3 jit environment', () => { // A few such properties are string constants. Ignore them, and focus on ExternalReferences. .filter(isExternalReference) // Some references are to interface types. Only take properties which have runtime values. - .filter(sym => !INTERFACE_EXCEPTIONS.has(sym.name)) + .filter(sym => !INTERFACE_EXCEPTIONS.has(sym.name) && !PARTIAL_ONLY.has(sym.name)) .forEach(sym => { // Assert that angularCoreEnv has a reference to the runtime symbol. expect(angularCoreEnv.hasOwnProperty(sym.name))