diff --git a/goldens/public-api/core/core.d.ts b/goldens/public-api/core/core.d.ts index 0bbed1ad02..728e56ac64 100644 --- a/goldens/public-api/core/core.d.ts +++ b/goldens/public-api/core/core.d.ts @@ -1,3 +1,6 @@ +/** @codeGenApi */ +export declare function $ngDeclareDirective(decl: unknown): unknown; + export declare interface AbstractType extends Function { prototype: T; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 2505b3ef44..304da8b032 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileDirectiveFromMetadata, ConstantPool, Expression, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler'; +import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveDef, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -154,19 +154,34 @@ export class DirectiveDecoratorHandler implements compileFull( node: ClassDeclaration, analysis: Readonly, resolution: Readonly, pool: ConstantPool): CompileResult[] { - const meta = analysis.meta; - const res = compileDirectiveFromMetadata(meta, pool, makeBindingParser()); - const factoryRes = compileNgFactoryDefField( - {...meta, injectFn: Identifiers.directiveInject, target: R3FactoryTarget.Directive}); + const def = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser()); + return this.compileDirective(analysis, def); + } + + compilePartial( + node: ClassDeclaration, analysis: Readonly, + resolution: Readonly): CompileResult[] { + const def = compileDeclareDirectiveFromMetadata(analysis.meta); + return this.compileDirective(analysis, def); + } + + private compileDirective( + analysis: Readonly, + {expression: initializer, type}: R3DirectiveDef): CompileResult[] { + const factoryRes = compileNgFactoryDefField({ + ...analysis.meta, + injectFn: Identifiers.directiveInject, + target: R3FactoryTarget.Directive, + }); if (analysis.metadataStmt !== null) { factoryRes.statements.push(analysis.metadataStmt); } return [ factoryRes, { name: 'ɵdir', - initializer: res.expression, + initializer, statements: [], - type: res.type, + type, } ]; } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 2a2da50088..2a5c16f61a 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -249,7 +249,10 @@ export class ExpressionTranslatorVisitor implements o.E visitExternalExpr(ast: o.ExternalExpr, _context: Context): TExpression { if (ast.value.name === null) { - throw new Error(`Import unknown module or symbol ${ast.value}`); + if (ast.value.moduleName === null) { + throw new Error('Invalid import without name nor moduleName'); + } + return this.imports.generateNamespaceImport(ast.value.moduleName); } // If a moduleName is specified, this is a normal import. If there's no module name, it's a // reference to a global/ambient symbol. diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index e60ef28312..84d6e2d30c 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 {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/render3/partial/api.ts b/packages/compiler/src/render3/partial/api.ts new file mode 100644 index 0000000000..b02a3a99c5 --- /dev/null +++ b/packages/compiler/src/render3/partial/api.ts @@ -0,0 +1,157 @@ +/** + * @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 o from '../../output/output_ast'; + +/** + * This interface describes the shape of the object that partial directive declarations are compiled + * into. This serves only as documentation, as conformance of this interface is not enforced during + * the generation of the partial declaration, nor when the linker applies full compilation from the + * partial declaration. + */ +export interface R3DeclareDirectiveMetadata { + /** + * Version number of the metadata format. This is used to evolve the metadata + * interface later - the linker will be able to detect which version a library + * is using and interpret its metadata accordingly. + */ + version: 1; + + /** + * Unparsed selector of the directive. + */ + selector?: string; + + /** + * Reference to the directive class itself. + */ + type: o.Expression; + + /** + * A mapping of inputs from class property names to binding property names, or to a tuple of + * binding property name and class property name if the names are different. + */ + inputs?: {[classPropertyName: string]: string|[string, string]}; + + /** + * A mapping of outputs from class property names to binding property names, or to a tuple of + * binding property name and class property name if the names are different. + */ + outputs?: {[classPropertyName: string]: string}; + + /** + * Information about host bindings present on the component. + */ + host?: { + /** + * A mapping of attribute names to their value expression. + */ + attributes?: {[key: string]: o.Expression}; + + /** + * A mapping of event names to their unparsed event handler expression. + */ + listeners: {[key: string]: string}; + + /** + * A mapping of bound properties to their unparsed binding expression. + */ + properties?: {[key: string]: string}; + + /** + * The value of the class attribute, if present. This is stored outside of `attributes` as its + * string value must be known statically. + */ + classAttribute?: string; + + /** + * The value of the style attribute, if present. This is stored outside of `attributes` as its + * string value must be known statically. + */ + styleAttribute?: string; + }; + + /** + * Information about the content queries made by the directive. + */ + queries?: R3DeclareQueryMetadata[]; + + /** + * Information about the view queries made by the directive. + */ + viewQueries?: R3DeclareQueryMetadata[]; + + /** + * The list of providers provided by the directive. + */ + providers?: o.Expression; + + /** + * The names by which the directive is exported. + */ + exportAs?: string[]; + + /** + * Whether the directive has an inheritance clause. Defaults to false. + */ + usesInheritance?: boolean; + + /** + * Whether the directive implements the `ngOnChanges` hook. Defaults to false. + */ + usesOnChanges?: boolean; + + /** + * A reference to the `@angular/core` ES module, which allows access + * to all Angular exports, including Ivy instructions. + */ + ngImport: o.Expression; +} + +export interface R3DeclareQueryMetadata { + /** + * Name of the property on the class to update with query results. + */ + propertyName: string; + + /** + * Whether to read only the first matching result, or an array of results. Defaults to false. + */ + first?: boolean; + + /** + * Either an expression representing a type or `InjectionToken` for the query + * predicate, or a set of string selectors. + */ + predicate: o.Expression|string[]; + + /** + * Whether to include only direct children or all descendants. Defaults to false. + */ + descendants?: boolean; + + /** + * An expression representing a type to read from each matched node, or null if the default value + * for a given node is to be returned. + */ + read?: o.Expression; + + /** + * Whether or not this query should collect only static results. Defaults to false. + * + * If static is true, the query's results will be set on the component after nodes are created, + * but before change detection runs. This means that any results that relied upon change detection + * to run (e.g. results inside *ngIf or *ngFor views) will not be collected. Query results are + * available in the ngOnInit hook. + * + * If static is false, the query's results will be set on the component after change detection + * runs. This means that the query results can contain nodes inside *ngIf or *ngFor views, but + * the results will not be available in the ngOnInit hook (only in the ngAfterContentInit for + * content hooks and ngAfterViewInit for view hooks). + */ + static?: boolean; +} diff --git a/packages/compiler/src/render3/partial/directive.ts b/packages/compiler/src/render3/partial/directive.ts new file mode 100644 index 0000000000..71496c4225 --- /dev/null +++ b/packages/compiler/src/render3/partial/directive.ts @@ -0,0 +1,143 @@ +/** + * @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 o from '../../output/output_ast'; +import {Identifiers as R3} from '../r3_identifiers'; +import {R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from '../view/api'; +import {createDirectiveTypeParams} from '../view/compiler'; +import {asLiteral, conditionallyCreateMapObjectLiteral, DefinitionMap} from '../view/util'; + + +/** + * Compile a directive declaration defined by the `R3DirectiveMetadata`. + */ +export function compileDeclareDirectiveFromMetadata(meta: R3DirectiveMetadata): R3DirectiveDef { + const definitionMap = createDirectiveDefinitionMap(meta); + + const expression = o.importExpr(R3.declareDirective).callFn([definitionMap.toLiteralMap()]); + + const typeParams = createDirectiveTypeParams(meta); + const type = o.expressionType(o.importExpr(R3.DirectiveDefWithMeta, typeParams)); + + return {expression, type}; +} + +/** + * Gathers the declaration fields for a directive into a `DefinitionMap`. This allows for reusing + * this logic for components, as they extend the directive metadata. + */ +export function createDirectiveDefinitionMap(meta: R3DirectiveMetadata): DefinitionMap { + const definitionMap = new DefinitionMap(); + + definitionMap.set('version', o.literal(1)); + + // e.g. `type: MyDirective` + definitionMap.set('type', meta.internalType); + + // e.g. `selector: 'some-dir'` + if (meta.selector !== null) { + definitionMap.set('selector', o.literal(meta.selector)); + } + + definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true)); + definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs)); + + definitionMap.set('host', compileHostMetadata(meta.host)); + + definitionMap.set('providers', meta.providers); + + if (meta.queries.length > 0) { + definitionMap.set('queries', o.literalArr(meta.queries.map(compileQuery))); + } + if (meta.viewQueries.length > 0) { + definitionMap.set('viewQueries', o.literalArr(meta.viewQueries.map(compileQuery))); + } + + if (meta.exportAs !== null) { + definitionMap.set('exportAs', asLiteral(meta.exportAs)); + } + + if (meta.usesInheritance) { + definitionMap.set('usesInheritance', o.literal(true)); + } + if (meta.lifecycle.usesOnChanges) { + definitionMap.set('usesOnChanges', o.literal(true)); + } + + definitionMap.set('ngImport', o.importExpr(R3.core)); + + return definitionMap; +} + +/** + * Compiles the metadata of a single query into its partial declaration form as declared + * by `R3DeclareQueryMetadata`. + */ +function compileQuery(query: R3QueryMetadata): o.LiteralMapExpr { + const meta = new DefinitionMap(); + meta.set('propertyName', o.literal(query.propertyName)); + if (query.first) { + meta.set('first', o.literal(true)); + } + meta.set( + 'predicate', Array.isArray(query.predicate) ? asLiteral(query.predicate) : query.predicate); + if (query.descendants) { + meta.set('descendants', o.literal(true)); + } + meta.set('read', query.read); + if (query.static) { + meta.set('static', o.literal(true)); + } + return meta.toLiteralMap(); +} + +/** + * Compiles the host metadata into its partial declaration form as declared + * in `R3DeclareDirectiveMetadata['host']` + */ +function compileHostMetadata(meta: R3HostMetadata): o.LiteralMapExpr|null { + const hostMetadata = new DefinitionMap(); + hostMetadata.set('attributes', toOptionalLiteralMap(meta.attributes, expression => expression)); + hostMetadata.set('listeners', toOptionalLiteralMap(meta.listeners, o.literal)); + hostMetadata.set('properties', toOptionalLiteralMap(meta.properties, o.literal)); + + if (meta.specialAttributes.styleAttr) { + hostMetadata.set('styleAttribute', o.literal(meta.specialAttributes.styleAttr)); + } + if (meta.specialAttributes.classAttr) { + hostMetadata.set('classAttribute', o.literal(meta.specialAttributes.classAttr)); + } + + if (hostMetadata.values.length > 0) { + return hostMetadata.toLiteralMap(); + } else { + return null; + } +} + +/** + * Creates an object literal expression from the given object, mapping all values to an expression + * using the provided mapping function. If the object has no keys, then null is returned. + * + * @param object The object to transfer into an object literal expression. + * @param mapper The logic to use for creating an expression for the object's values. + * @returns An object literal expression representing `object`, or null if `object` does not have + * any keys. + */ +function toOptionalLiteralMap( + object: {[key: string]: T}, mapper: (value: T) => o.Expression): o.LiteralMapExpr|null { + const entries = Object.keys(object).map(key => { + const value = object[key]; + return {key, value: mapper(value), quoted: true}; + }); + + if (entries.length > 0) { + return o.literalMap(entries); + } else { + return null; + } +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 87e108d8e4..9809447561 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -16,6 +16,8 @@ export class Identifiers { static TRANSFORM_METHOD = 'transform'; static PATCH_DEPS = 'patchedDeps'; + static core: o.ExternalReference = {name: null, moduleName: CORE}; + /* Instructions */ static namespaceHTML: o.ExternalReference = {name: 'ɵɵnamespaceHTML', moduleName: CORE}; @@ -245,10 +247,8 @@ export class Identifiers { moduleName: CORE, }; - static defineDirective: o.ExternalReference = { - name: 'ɵɵdefineDirective', - moduleName: CORE, - }; + static defineDirective: o.ExternalReference = {name: 'ɵɵdefineDirective', moduleName: CORE}; + static declareDirective: o.ExternalReference = {name: '$ngDeclareDirective', moduleName: CORE}; static DirectiveDefWithMeta: o.ExternalReference = { name: 'ɵɵDirectiveDefWithMeta', diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 162f74fdfa..8e92fe39a9 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -86,12 +86,14 @@ export interface R3DirectiveMetadata { }; /** - * A mapping of input field names to the property names. + * A mapping of inputs from class property names to binding property names, or to a tuple of + * binding property name and class property name if the names are different. */ inputs: {[field: string]: string|[string, string]}; /** - * A mapping of output field names to the property names. + * A mapping of outputs from class property names to binding property names, or to a tuple of + * binding property name and class property name if the names are different. */ outputs: {[field: string]: string}; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 790a14df23..cc72464a9d 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -492,7 +492,7 @@ function stringArrayAsType(arr: ReadonlyArray): o.Type { o.NONE_TYPE; } -function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] { +export function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] { // On the type side, remove newlines from the selector as it will need to fit into a TypeScript // string literal, which must be on one line. const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 07ccc60cf2..f061893474 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -77,6 +77,8 @@ export { NG_PIPE_DEF as ɵNG_PIPE_DEF, } from './render3/fields'; export { + + $ngDeclareDirective, AttributeMarker as ɵAttributeMarker, ComponentDef as ɵComponentDef, ComponentFactory as ɵRender3ComponentFactory, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index f6c8aafd5e..cfdf8187d9 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -133,6 +133,9 @@ export { AttributeMarker } from './interfaces/node'; export {CssSelectorList, ProjectionSlots} from './interfaces/projection'; +export { + $ngDeclareDirective, +} from './jit/partial'; export { setClassMetadata, } from './metadata'; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 9f028b1d43..ce4bb0179f 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -10,6 +10,7 @@ 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,4 +170,6 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵtrustConstantHtml': sanitization.ɵɵtrustConstantHtml, 'ɵɵtrustConstantScript': sanitization.ɵɵtrustConstantScript, 'ɵɵtrustConstantResourceUrl': sanitization.ɵɵtrustConstantResourceUrl, + + '$ngDeclareDirective': partial.$ngDeclareDirective, }))(); diff --git a/packages/core/src/render3/jit/partial.ts b/packages/core/src/render3/jit/partial.ts new file mode 100644 index 0000000000..9ba050fba6 --- /dev/null +++ b/packages/core/src/render3/jit/partial.ts @@ -0,0 +1,16 @@ +/** + * @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 + */ + +/** + * Compiles a partial directive declaration object into a full directive definition object. + * + * @codeGenApi + */ +export function $ngDeclareDirective(decl: unknown): unknown { + throw new Error('Not yet implemented'); +}